关于PyQt5+Python3开发环境安装可以参考上篇。这里实现的是一种群聊工具,socket类使用的是Qt的TcpSocket, TcpServer 类;线程类使用的QTread;收发数据使用的QDataStream(当然也可以用其他的方式,不唯一)
github链接: https://github.com/ielcome2017/Chat.git
先看服务器端, 服务端界面设计,下面标注红色的是后面代码中要使用的控件,其它的可要可不要 # Ui_server.py
# -*- coding: utf-8 -*-
#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(541, 355)
self.splitter_2 = QtWidgets.QSplitter(Form)
self.splitter_2.setGeometry(QtCore.QRect(10, 10, 516, 331))
self.splitter_2.setOrientation(QtCore.Qt.Vertical)
self.splitter_2.setObjectName("splitter_2")
self.layoutWidget = QtWidgets.QWidget(self.splitter_2)
self.layoutWidget.setObjectName("layoutWidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.label = QtWidgets.QLabel(self.layoutWidget)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
self.txtPort = QtWidgets.QLineEdit(self.layoutWidget)
self.txtPort.setObjectName("txtPort")
self.horizontalLayout.addWidget(self.txtPort)
self.btnCreate = QtWidgets.QPushButton(self.layoutWidget)
self.btnCreate.setObjectName("btnCreate")
self.horizontalLayout.addWidget(self.btnCreate)
self.splitter = QtWidgets.QSplitter(self.splitter_2)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter")
self.layoutWidget1 = QtWidgets.QWidget(self.splitter)
self.layoutWidget1.setObjectName("layoutWidget1")
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget1)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.lbUserInfo = QtWidgets.QLabel(self.layoutWidget1)
self.lbUserInfo.setObjectName("lbUserInfo")
self.verticalLayout.addWidget(self.lbUserInfo)
self.listUser = QtWidgets.QListWidget(self.layoutWidget1)
self.listUser.setObjectName("listUser")
self.verticalLayout.addWidget(self.listUser)
self.layoutWidget2 = QtWidgets.QWidget(self.splitter)
self.layoutWidget2.setObjectName("layoutWidget2")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.layoutWidget2)
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.chatInfo = QtWidgets.QLabel(self.layoutWidget2)
self.chatInfo.setObjectName("chatInfo")
self.verticalLayout_2.addWidget(self.chatInfo)
self.browerChat = QtWidgets.QTextBrowser(self.layoutWidget2)
self.browerChat.setObjectName("browerChat")
self.verticalLayout_2.addWidget(self.browerChat)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
self.label.setText(_translate("Form", "ADDRESS:PORT:"))
self.btnCreate.setText(_translate("Form", "create"))
self.lbUserInfo.setText(_translate("Form", "用户列表"))
self.chatInfo.setText(_translate("Form", "聊天信息"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec_())
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec_())
服务器端实现:
在服务器端,使用QTcpServer类监听接入客户端,对于QTcpServer类,一旦有用户接入,就调用
其incomingConnection函数 。要使用多线程的话,就要在incomingConnection中调用线程类QThread,生成新的线程,
然后再新的线程run函数中调用QTcpSocket实例,进行数据的收发,在这里我也把QTcpSocket类重新实现了,也可以就在
重载的Thread实现QTcpSocket收发的代码,代码如下:
# -*- coding: utf-8 -*-
"""
Module implementing Server.
"""
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtNetwork import *
from Ui_server import Ui_Form
import sys
PORT =26711
SIZEOF_UINT16 = 2
class TcpSocket(QTcpSocket):
'''
实现了客户端读取信息与发送信息到客户端的功能
'''
#信号用于Thread类中,信号在接受客户端信息后发送,一直发送到Server更新其界面
#上级为Thead, 先将其交付给连接Thread中的slotRecv
signRecv= pyqtSignal(str, str)
def __init__(self, socketId, parent=None):
super(TcpSocket, self).__init__(parent)
self.socketId = socketId
#客户端发送信息就接受
self.readyRead.connect(self.slotRecv)
#接受信息槽函数, client-> slotRecv -> Server
def slotRecv(self):
#使用QDataStream接受信息,也可以选择其他接受方式
while self.state() == QAbstractSocket.ConnectedState:
nextBlockSize = 0
stream = QDataStream(self)
if self.bytesAvailable() >= SIZEOF_UINT16:
nextBlockSize = stream.readUInt16()
else:
#print('cannot read client')
break
if self.bytesAvailable() < nextBlockSize:
break
action = ''
msg = ''
action = stream.readQString()
msg = stream.readQString()
clientAddress = self.peerAddress().toString()
clientPort = str(self.peerPort())
msg= clientAddress+ ':'+clientPort + ' '+ msg+'\n'
#发射给上级Thread
self.signRecv.emit(action, msg)
#发送信息槽函数,其变量数据来源于上级 Server -> slotSend -> client
def slotSend(self, action, msg, id):
#print(id)
#print(int(self.socketId))
if id == int(self.socketId):
reply = QByteArray()
stream = QDataStream(reply, QIODevice.WriteOnly)
stream.writeUInt16(0)
stream.writeQString(action)
stream.writeQString(msg)
stream.device().seek(0)
stream.writeUInt16(reply.size() - SIZEOF_UINT16)
self.write(reply)
class Thread(QThread):
'''
线程类,在run中创建socket变量, 然后充当中介,向下交付slotSend中参数,
向上交付slotRecv中得到的参数
'''
#被TcpServer 使用,在类中发射
signRecv = pyqtSignal(str, str)
#在类中使用, 连接socket类中的slotSend
signSend = pyqtSignal(str, str, int)
def __init__(self, socketId, parent):
super(Thread, self).__init__(parent)
self.socketId = socketId
def run(self):
socket = TcpSocket(self.socketId)
if not socket.setSocketDescriptor(self.socketId):
return
#socket类中的signRecv信号在此连接,把socket接受到的信息作为参数传递给Thread的slotSend
socket.signRecv.connect(self.slotRecv)
#Thread类中的signSend连接socket中slotSend, 将从上级得到的信息传递给socket,最后发送到客户端
self.signSend.connect(socket.slotSend)
#循环,不加这一句会出问题
self.exec_()
def slotSend(self, action, msg, id):
if action == '':
return
#发射到socket中
self.signSend.emit(action, msg, id)
def slotRecv(self, action, msg):
#发射到TcpServer
self.signRecv.emit(action, msg)
class TcpServer(QTcpServer):
signRecv= pyqtSignal(str, str)
signSend = pyqtSignal(str, str, int)
#存放客户端socket的socketId
socketList = []
def __init__(self, parent=None):
super(TcpServer, self).__init__(parent)
def incomingConnection(self, socketId):
#一有客户端进来就加入列表,当然在此要判断列表是否存在该客户端
if socketId not in self.socketList:
self.socketList.append(socketId)
#创建线程
thread = Thread(socketId, self)
#thread发射后,调用tcpserver的slotRecv ,然后其又发射自身的signRecv信号,该信号在Server中连接Server的slotRecv
thread.signRecv.connect(self.slotRecv)
#连接thread的slotSend
self.signSend.connect(thread.slotSend)
thread.finished.connect(thread.deleteLater)
thread.start()
def slotRecv(self, action, msg):
self.signRecv.emit(action, msg)
class Server(QWidget, Ui_Form):
"""
Class documentation goes here.
"""
signSend = pyqtSignal(str, str, int)
def __init__(self, parent=None):
"""
Constructor
@param parent reference to the parent widget
@type QWidget
"""
super(Server, self).__init__(parent)
self.setupUi(self)
self.server= TcpServer(self)
self.server.listen(QHostAddress("0.0.0.0"), PORT)
#tcoServer中的signRecv信号,连接自身的slotRecv
self.server.signRecv.connect(self.slotRecv)
def slotRecv(self, action, msg):
self.updateServer(action, msg)
for id in self.server.socketList:
self.server.signSend.emit(action, msg, id)
def showConnection(self):
print(self.server.socketList)
#def slotSend(self, action, msg):
def updateServer(self, action, msg):
if action == 'USER':
self.listUser.addItem('*user*: '+ msg)
if action == 'MSG':
self.browerChat.append(msg)
@pyqtSlot()
def on_btnCreate_clicked(self):
"""
Slot documentation goes here.
"""
# TODO: not implemented yet
if __name__ == '__main__':
app = QApplication(sys.argv)
serverDlg = Server()
serverDlg.show()
app.exec_()
客户端实现, 客户端类似服务器端,只不过多了一个发送消息的LineEdit, 取名为txtInput, 发送按钮btnSend
代码(Ui_client.py):
# -*- coding: utf-8 -*-
#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(455, 366)
self.listUser = QtWidgets.QListWidget(Form)
self.listUser.setGeometry(QtCore.QRect(0, 10, 161, 341))
self.listUser.setObjectName("listUser")
self.splitter_2 = QtWidgets.QSplitter(Form)
self.splitter_2.setGeometry(QtCore.QRect(180, 10, 256, 341))
self.splitter_2.setOrientation(QtCore.Qt.Vertical)
self.splitter_2.setObjectName("splitter_2")
self.setNameWidget = QtWidgets.QWidget(self.splitter_2)
self.setNameWidget.setObjectName("setNameWidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.setNameWidget)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.lbName = QtWidgets.QLabel(self.setNameWidget)
self.lbName.setObjectName("lbName")
self.horizontalLayout_3.addWidget(self.lbName)
self.txtName = QtWidgets.QLineEdit(self.setNameWidget)
self.txtName.setObjectName("txtName")
self.horizontalLayout_3.addWidget(self.txtName)
self.btnSet = QtWidgets.QPushButton(self.setNameWidget)
self.btnSet.setObjectName("btnSet")
self.horizontalLayout_3.addWidget(self.btnSet)
self.horizontalLayout.addLayout(self.horizontalLayout_3)
self.browerChat = QtWidgets.QTextBrowser(self.splitter_2)
self.browerChat.setObjectName("browerChat")
self.layoutWidget = QtWidgets.QWidget(self.splitter_2)
self.layoutWidget.setObjectName("layoutWidget")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.layoutWidget)
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.txtInput = QtWidgets.QLineEdit(self.layoutWidget)
self.txtInput.setObjectName("txtInput")
self.horizontalLayout_2.addWidget(self.txtInput)
self.btnSend = QtWidgets.QPushButton(self.layoutWidget)
self.btnSend.setObjectName("btnSend")
self.horizontalLayout_2.addWidget(self.btnSend)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
self.lbName.setText(_translate("Form", "name"))
self.txtName.setText(_translate("Form", "deafault"))
self.btnSet.setText(_translate("Form", "SetConnection"))
self.btnSend.setText(_translate("Form", "Send"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec_())
client.py 代码:
# -*- coding: utf-8 -*-
"""
Module implementing Client.
"""
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtNetwork import *
from Ui_client import Ui_Form
import sys
import time
PORT = 26711
SIZEOF_UINT16 = 2
class Client(QWidget, Ui_Form):
"""
Class documentation goes here.
"""
def __init__(self, parent=None):
"""
Constructor
@param parent reference to the parent widget
@type QWidget
"""
super(Client, self).__init__(parent)
self.setupUi(self)
self.socket = QTcpSocket()
self.request = None
#self.nextBlockSize = 0
self.name = ''
#信号
#self.socket.connected.connected.connect(self.sendRequest)
self.socket.readyRead.connect(self.readResponse)
self.socket.disconnected.connect(self.serverHasStopped)
self.socket.error.connect(self.serverHasError)
self.socket.connectToHost("localhost", PORT)
@pyqtSlot()
def on_btnSend_clicked(self):
"""
Slot documentation goes here.
"""
# TODO: not implemented yet
msg =self.name +' '+\
time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))+'\n'+\
self.txtInput.text()
self.issueRequest('MSG', msg)
self.txtInput.setText('')
@pyqtSlot()
def on_txtInput_returnPressed(self):
"""
Slot documentation goes here.
"""
# TODO: not implemented yet
pass
@pyqtSlot()
def on_btnSet_clicked(self):
"""
Slot documentation goes here.
"""
# TODO: not implemented yet
self.name = self.txtName.text()
self.issueRequest('USER', self.name)
def issueRequest(self, action, msg):
print('sendRequest')
self.request= QByteArray()
stream = QDataStream(self.request, QIODevice.WriteOnly)
stream.writeUInt16(0)
stream.writeQString(action)
stream.writeQString(msg)
stream.device().seek(0)
stream.writeUInt16(self.request.size() - SIZEOF_UINT16)#overwrite seek(0)
self.socket.write(self.request)
self.nextBlockSize = 0
self.requst = None
def readResponse(self):
nextBlockSize = 0
stream = QDataStream(self.socket)
while 1:
if nextBlockSize == 0 :
if self.socket.bytesAvailable() <SIZEOF_UINT16:
break
nextBlockSize = stream.readUInt16()
if self.socket.bytesAvailable() < nextBlockSize:
break
action = ''
msg = ''
action = stream.readQString()
msg = stream.readQString()
print(msg)
if action == 'MSG':
self.browerChat.append(msg)
if action == 'USER':
self.listUser.addItem('*user*: '+ msg)
def serverHasStopped(self):
#self.issueRequest('CLOSE', self.name)
#self.socket.close()
print('socket is close')
def serverHasError(self, error):
self.socket.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
dlg = Client()
dlg.show()
sys.exit(app.exec_())
运行结果如下:
之前的设计中经常出现Cannot create children for a parent that is in a different thread
Socket notifiers cannot be enabled or disabled from another thread等问题
但是以上代码是都解决了这些问题,当然还有很多不足之处,比如说用户列表那一块,懒得搞了,以后再说吧