チャット クライアントを作成して PyQt+Twisted を実践しています。これには、2 つのイベント ループを適切に再生する必要があります。Twisted QTReactor を使わずにこれを行う方法を知りたいです。私が達成しようとしているパターンは
- Qt は通常どおり実行され、Twisted は別のスレッド (QThread など) で実行されます。
- Qt 関数/メソッドは、 を使用して Twisted 関数を呼び出します
callFromThread
。 - Twisted によって与えられた deferred をフックして、Qt シグナルの放出を引き起こします。このようにして、Twisted スレッドは Qt スレッドをコールバックできます。
私は実際にその部分を機能させました。ソリューションの核心はこの機能です
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback
func is the Twisted function you want to invoke. It probably returns a
deferred. successSignal is the signal you want to emit once the asynchronous
call finishes. This signal will be emitted with the result of func as an
argument
"""
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 スレッドに取得できるようにする必要があります。この場合、プログラムの Twisted 部分にコールバックとして使用するシグナルを提供していないため、これは難しいように思えます。どうやってやるの?
以下に、ほぼ動作するチャット クライアントとサーバーを実行するための完全なサンプル コードを示します。これらのプログラムを使用するには、まずサーバーとクライアントを .py ファイルとしてコピーし、UI 仕様を .ui ファイルとしてコピーします。サーバーを実行してから、クライアントを実行します。クライアント ウィンドウが表示されたら、[接続] ボタンをクリックします。新しい接続を示すメッセージがサーバーに表示されます。次に、行編集に入力して、「送信」ボタンをクリックしてみてください。データがサーバーに到達して戻ってくることがわかりますが、クライアントの QTextEdit ボックスには表示されません。これは、ChatProtocol からプログラムの Qt 部分にデータを取得する方法がわからないためです。どうすればこれを行うことができますか?
サーバー (非同期コア)
import asyncore
import socket
import constants as C
HOST = 'localhost'
PORT = 12344
class ChatServer(asyncore.dispatcher):
"""Receive and forward chat messages
When a new connection is made we spawn a dispatcher for that
connection.
"""
ADDRESS_FAMILY = socket.AF_INET
SOCKET_TYPE = socket.SOCK_STREAM
def __init__(self, host, port):
self.map = {}
self.address = (host,port)
self.clients = []
asyncore.dispatcher.__init__(self, map=self.map)
def serve(self):
"""Bind to socket and start asynchronous loop"""
self.create_socket(self.ADDRESS_FAMILY, self.SOCKET_TYPE)
self.bind(self.address)
print("ChatServer bound to %s %s"%self.address)
self.listen(1)
asyncore.loop(map=self.map)
def writable(self):
return False
def readable(self):
return True
def newMessage(self, data, fromWho):
"""Put data in all clients' buffers"""
print("new data: %s"%data)
for client in self.clients:
client.buffer = client.buffer + data
def handle_accept(self):
"""Deal with newly accepted connection"""
(connSock, clientAddress) = self.accept()
print("New connection accepted from %s %s"%clientAddress)
self.clients.append(ChatHandler(connSock, self.map, self))
class ChatHandler(asyncore.dispatcher):
def __init__(self, sock, map, server):
self.server = server
self.buffer = ''
asyncore.dispatcher.__init__(self, sock, map)
def writable(self):
return len(self.buffer) > 0
def readable(self):
return True
def handle_read(self):
"""Notify server of any new incoming data"""
data = self.recv(4096)
if data:
self.server.newMessage(data, self)
def handle_write(self):
"""send some amount of buffer"""
sent = self.send(self.buffer)
self.buffer = self.buffer[sent:]
if __name__=='__main__':
if HOST is None:
HOST = raw_input('Host: ')
if PORT is None:
PORT = int(raw_input('Port: '))
s = ChatServer(HOST, PORT)
s.serve()
クライアント (PyQt + ツイスト)
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
class MainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.ui = uic.loadUi('ui.ui')
self.networkThread = NetworkThread()
self.networkThread.start()
self.ui.sendButton.clicked.connect(self.sendMessage)
self.ui.connectButton.clicked.connect(self.getNetworkConnection)
#Make connections from network thread signals to our slots
self.connect(self.networkThread,
self.networkThread.sigConnected,
self.onConnected)
self.connect(self.networkThread,
self.networkThread.sigNewData,
self.onNewData)
self.ui.show()
def getNetworkConnection(self):
factory = protocol.ClientCreator(reactor, ChatProtocol)
self.networkThread.callFromMain(factory.connectTCP,
self.networkThread.sigConnected,
'localhost', 12344)
def onConnected(self, p):
print("Got a protocol!")
self.cxn = p
def onNewData(self, data):
self.ui.outputBox.append('\r\n'+data)
def sendMessage(self):
message = str(self.ui.inputBox.text())
self.networkThread.callFromMain(self.cxn.send, None, message)
class NetworkThread(QtCore.QThread):
"""Run the twisted reactor in its own thread"""
def __init__(self):
QtCore.QThread.__init__(self)
self.sigConnected = QtCore.SIGNAL("sigConnected")
self.sigNewData = QtCore.SIGNAL("sigNewData")
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 ChatProtocol(protocol.Protocol):
def dataReceived(self, data):
print("Got data: %s"%data)
print("...but I don't know how to pass it to Qt :(")
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_())
UI ファイル
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTextEdit" name="outputBox"/>
</item>
<item>
<widget class="QLineEdit" name="inputBox"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="sendButton">
<property name="text">
<string>send</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connectButton">
<property name="text">
<string>connect</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>