模拟QQ聊天界面遇到的问题:关于PyQt5 GUI模块不允许在多线程中进行操作的解决办法

简介

今天想要使用PyQt5结合Websocket实现一个小小的QQ聊天界面。

介绍一下我实现这个功能的具体思路:GUI界面运行起来后,创建一个线程去连接Websocket服务器,然后主界面类中实现了websocket的基本回调函数。比如,发送消息的回调函数send_message、接收消息的回调函数on_message等等。当接收到消息时,会在线程中调用on_message函数来对接收到的消息进行处理。原本我计划是在这个回调函数中进行对界面上的聊天消息界面进行更新的。结果就在这里遇到了这个问题,阻挡了我前进的步伐。就是PyQt5 GUI模块不允许在多线程中进行操作。也就是说,咱们在线程中是不能对界面GUI模块进行修改的。(应该是吧)

原本的代码

import json
import sys
import threading

import pymysql
from PyQt5 import QtGui, QtWidgets, QtCore
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QApplication, QGraphicsDropShadowEffect, QMessageBox, QListWidgetItem
from PyQt5.QtCore import Qt

from Assets.MessageItem import OtherMessageItem
from Assets.MessageItemMy import MyMessageItem
from ui.chat_page import Ui_Form
import websocket



class chatPage(QtWidgets.QWidget, Ui_Form):

    def __init__(self, my_user_id, other_user_id):
        super(chatPage, self).__init__()
        self.setupUi(self)  # 初始化Ui函
        self.client_id = my_user_id
        self.other_id = other_user_id
        self.history_message = []  # 历史消息列表

        self.widget_2.setLayout(QtWidgets.QVBoxLayout())
        self.widget_2.setStyleSheet("border:none; background:transparent;")
       
        # 连接WebSocket服务器
        uri = f"ws://localhost:8000/ws/{self.client_id}"
        self.ws = websocket.WebSocketApp(uri,
                                         on_message=self.on_message,
                                         on_error=self.on_error,
                                         on_close=self.on_close,
                                         on_open=self.send_message)
        self.init_ui()  # 初始化界面
        self.init_solt()  # 初始化槽函数

        # 在单独的线程中启动 WebSocket
        self.ws_thread = threading.Thread(target=self.ws.run_forever)
        self.ws_thread.start()


    def init_ui(self):
        """
        :return:
        """
        Qt.FramelessWindowHint无边框窗口特性。在没有边框的情况下,窗口的默认行为可能不再包含拖动窗口的功能。
        如果您希望添加阴影效果却又想要保留移动窗口的功能,您可以考虑实现自定义的拖动窗口功能。这涉及到捕获鼠标按下、移动和释放事件,并据此更新窗口的位置。
        self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.Tool)  # 窗口置顶,无边框,在任务栏不显示图标
        shadow = QGraphicsDropShadowEffect()  # 设定一个阴影,半径为10,颜色为#444444,定位为0,0
        shadow.setBlurRadius(10)
        shadow.setColor(QColor("#444444"))
        shadow.setOffset(0, 0)
        self.frame.setGraphicsEffect(shadow)  # 为frame设定阴影效果
        self.init_history_message()

    def init_solt(self):
        """
        初始化槽函数
        :return:
        """
        self.pushBtn.clicked.connect(self.send_message)  # 按钮发送消息

    def init_history_message(self):
        """
        初始化好友列表
        :return:
        """
        pass
    def show_history_message(self):
        """
        初始化聊天记录
        :return:
        """
        self.chat_list = QtWidgets.QListWidget()

        self.chat_list.setItemDelegate(NoHoverDelegate(self.chat_list))

        # 显示聊天记录
        for items in self.history_message:
            print(items)
            sender = items[1]
            if sender == self.client_id:
                item = MyMessageItem(data=items[0])
            else:
                item = OtherMessageItem(data=items[0])
            listwitem = QListWidgetItem(self.chat_list)
            listwitem.setSizeHint(QtCore.QSize(200, 70))
            self.chat_list.setItemWidget(listwitem, item)


        layout = self.widget_2.layout()

        layout.addWidget(self.chat_list)
        self.chat_list.update()
        self.chat_list.scrollToBottom()  # 自动滚动到底部
        print('聊天记录显示完成')

    def send_message(self):
        """
        发送消息
        :return:
        """
        message = self.textEdit.toPlainText()
        if message:  # 只有当消息不为空时才发送
            message_dict = {
                "receiver_id": self.other_id,
                "content": message,
                "client_id": self.client_id
            }
            self.ws.send(json.dumps(message_dict))
            self.textEdit.clear()  # 清空输入框
            return
        QMessageBox.information(self, "提示", "发送的消息不能为空")

    def on_message(self, ws, message):
        """
        # 接收消息回调函数
        :param message:
        :return:
        """
        if message == '消息已发送给对方' or message == '对方不在线,消息已发布到RabbitMQ,稍后将会推送给对方':
            print('接收到消息:', message)
            return
        # 原本是在这里对界面进行更新
        print(message)
        data = (message, self.other_id)
        self.history_message.append(data)
        # 刷新界面
        # self.show_history_message()
        sender = self.other_id
        item = MyMessageItem(data=message) if sender == self.client_id else OtherMessageItem(data=message)
        listwitem = QListWidgetItem(self.chat_list)
        listwitem.setSizeHint(QtCore.QSize(200, 70))
        self.chat_list.addItem(listwitem)
        self.chat_list.setItemWidget(listwitem, item)
        self.chat_list.scrollToBottom()  # 自动滚动到底部
        self.chat_list.update()

    def on_error(self, ws, error):
        """
        # 错误回调函数
        :param ws:
        :param error:
        :return:
        """
        pass

    def on_close(self, ws):
        """
        # 关闭websocket回调函数
        :param ws:
        """
        ws.close()

    def mousePressEvent(self, event):
        self.click_pos = event.globalPos()

    def mouseMoveEvent(self, event):
        if self.click_pos:
            delta = event.globalPos() - self.click_pos
            self.move(self.pos() + delta)
            self.click_pos = event.globalPos()

    def mouseReleaseEvent(self, event):
        self.click_pos = None


if __name__ == '__main__':
    app = QApplication(sys.argv)
    login_page = chatPage('738053369', '545247018')
    login_page.show()
    sys.exit(app.exec_())

解决思路

既然线程中对GUI界面进行操作,那么我们只能在主线程对新消息进行处理并显示到界面上。所以

 ,我初始化的时候,定义了一个定时器,和一个用来存储接收到消息的列表。当on_message回调函数接收到新消息时会将接收到的消息存储到这个列表中。定时器会定时地去检查这个列表,一旦发现这个列表不为空,那么就将列表中的消息取出然后刷新聊天界面。

具体代码:

# encoding: utf-8
# @author: DayDreamer
# @file: chat_page.py
# @time: 2024/6/27 20:34
# @desc:
"""
Your time is limited,So don't waste it living in someone else's life.
And most important,
Have the courage to follow your heart and intuition.
They somehow already know
What you truly want to become,Everything else is secondary。
"""
import asyncio
import hashlib
import json
import random
import sys
import threading

import pymysql
from PyQt5 import QtGui, QtWidgets, QtCore
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QApplication, QGraphicsDropShadowEffect, QMessageBox, QListWidgetItem
from PyQt5.QtCore import Qt, QTimer, QSize

from Assets.MessageItem import OtherMessageItem
from Assets.MessageItemMy import MyMessageItem
from db.mysql_orm.crud import Register_new_user
from lib.encrypted import encrypted_pwd
from lib.new_account import create_new_account
from lib.sent_account import message_sent_account
from lib.sql_command import is_exists
from ui.chat_page import Ui_Form
import websocket


class NoHoverDelegate(QtWidgets.QStyledItemDelegate):
    """自定义委托以禁用悬停效果"""

    def paint(self, painter, option, index):
        if option.state & QtWidgets.QStyle.State_MouseOver:
            option.state = option.state & ~QtWidgets.QStyle.State_MouseOver
        super().paint(painter, option, index)


class chatPage(QtWidgets.QWidget, Ui_Form):
    # registered_window = QtCore.pyqtSignal()  # 跳转信号

    def __init__(self, my_user_id, other_user_id):
        super(chatPage, self).__init__()
        self.setupUi(self)  # 初始化Ui函
        self.flag = False  # 标记是否已发送消息
        self.client_id = my_user_id
        self.other_id = other_user_id
        # 创建一个定时器,用于定时地更新聊天记录
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_chat_list)
        self.history_message_new = []  # 历史消息列表,用于接收新的消息
        self.timer.start(1000)  # 1000ms刷新一次

        self.history_message = []  # 历史消息列表
        self.history_message = []  # 历史消息列表

        self.widget_2.setLayout(QtWidgets.QVBoxLayout())
        self.widget_2.setStyleSheet("border:none; background:transparent;")

        # 连接Websocket服务器
        uri = f"ws://localhost:8000/ws/{self.client_id}"
        self.ws = websocket.WebSocketApp(uri,
                                         on_message=self.on_message,
                                         on_error=self.on_error,
                                         on_close=self.on_close,
                                         on_open=self.send_message)
        self.init_ui()  # 初始化界面
        self.init_solt()  # 初始化槽函数

        # 在单独的线程中启动 WebSocket
        self.ws_thread = threading.Thread(target=self.ws.run_forever)
        self.ws_thread.start()


    def init_ui(self):
        """
        # Author: Daydreamer
        初始化界面
        :return:
        """
        """
        Qt.FramelessWindowHint无边框窗口特性。在没有边框的情况下,窗口的默认行为可能不再包含拖动窗口的功能。
        如果您希望添加阴影效果却又想要保留移动窗口的功能,您可以考虑实现自定义的拖动窗口功能。这涉及到捕获鼠标按下、移动和释放事件,并据此更新窗口的位置。
        :return: 
        """
        self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.Tool)  # 窗口置顶,无边框,在任务栏不显示图标
        shadow = QGraphicsDropShadowEffect()  # 设定一个阴影,半径为10,颜色为#444444,定位为0,0
        shadow.setBlurRadius(10)
        shadow.setColor(QColor("#444444"))
        shadow.setOffset(0, 0)
        self.frame.setGraphicsEffect(shadow)  # 为frame设定阴影效果
        self.init_history_message()  # 初始化聊天记录

    def init_solt(self):
        """
        初始化槽函数
        :return:
        """
        self.pushBtn.clicked.connect(self.send_message)  # 按钮发送消息

    def init_rabbitmq_history_message(self):
        """
        # Author: Daydreamer
        初始化RabbitMQ中的聊天记录,这些聊天记录是在用户离线时该好友发送给他的消息,在用户上线后,该好友会将这些消息推送给他。
        :return:
        """
        pass

    def init_history_message(self):
        """
        # Author: Daydreamer
        初始化和该好友的历史聊天记录
        :return:
        """
        pass


    def show_history_message(self):
        """
        # Author: Daydreamer
        初始化聊天记录,将历史消息显示在聊天列表中
        :return:
        """
        self.chat_list = QtWidgets.QListWidget()  # 创建一个聊天列表

        self.chat_list.setItemDelegate(NoHoverDelegate(self.chat_list))  # 禁用悬停效果

        # 遍历聊天记录列表,将消息显示在聊天列表中
        for items in self.history_message:
            print(items)
            sender = items[1]
            if sender == self.client_id:  # 判断消息的发送者是自己还是对方
                item = MyMessageItem(data=items[0])
            else:
                item = OtherMessageItem(data=items[0])
            listwitem = QListWidgetItem(self.chat_list)
            listwitem.setSizeHint(QtCore.QSize(200, 70))
            self.chat_list.setItemWidget(listwitem, item)

        layout = self.widget_2.layout()

        layout.addWidget(self.chat_list)
        self.chat_list.update()
        self.chat_list.scrollToBottom()  # 自动滚动到底部
        print('聊天记录显示完成')


    def send_message(self):
        """
        # Author: Daydreamer
        发送消息到Websocket服务器
        :return:
        """
        message = self.textEdit.toPlainText()
        if message:  # 只有当消息不为空时才发送
            message_dict = {
                "receiver_id": self.other_id,
                "content": message,
                "client_id": self.client_id
            }
            self.ws.send(json.dumps(message_dict))
            self.textEdit.clear()  # 清空输入框
            # TODO 将消息保存到数据库
            return
        QMessageBox.information(self, "提示", "发送的消息不能为空")

    def on_message(self, ws, message):
        """
        # Author: Daydreamer
        # 接收消息回调函数
        :param message:
        :return:
        """
        # ATTENTION: 这里的on_message是在线程中执行的,所以,在这里面进行界面更新是不对的,应该在主线程中进行界面更新(因为PyQt5的GUI模块是不允许在多线程中进行操作的)
        if message == '消息已发送给对方' or message == '对方不在线,消息已发布到RabbitMQ,稍后将会推送给对方':
            print('接收到消息:', message)
            return
        print(message)
        data = (message, self.other_id)
        self.history_message_new.append(data)  # 将接收到的消息添加到历史消息列表中,方便PyQt的定时任务刷新界面

    def update_chat_list(self):
        """
        # Author: Daydreamer
        # 定时刷新聊天记录
        :return:
        """
        if len(self.history_message_new) > 0:
            print('开始更新界面')
            item = OtherMessageItem(data=self.history_message_new[0][0])
            listwitem = QListWidgetItem(self.chat_list)
            listwitem.setSizeHint(QSize(200, 70))

            self.chat_list.addItem(listwitem)
            self.chat_list.setItemWidget(listwitem, item)
            self.chat_list.scrollToBottom()
            print('刷新成功')
            # 将历史消息列表清空, 以方便新消息的接收
            self.history_message_new.clear()

    def on_error(self, ws, error):
        """
        # 错误回调函数
        :param ws:
        :param error:
        :return:
        """
        pass

    def on_close(self, ws):
        """
        # 关闭websocket回调函数
        :param ws:
        """
        ws.close()

    def mousePressEvent(self, event):
        self.click_pos = event.globalPos()

    def mouseMoveEvent(self, event):
        if self.click_pos:
            delta = event.globalPos() - self.click_pos
            self.move(self.pos() + delta)
            self.click_pos = event.globalPos()

    def mouseReleaseEvent(self, event):
        self.click_pos = None


if __name__ == '__main__':
    app = QApplication(sys.argv)
    login_page = chatPage('545247018', '738053369')
    login_page.show()
    sys.exit(app.exec_())

最终效果

QQ录屏20240702164601

  • 11
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用PyQt5进行多线程工作的GUI示例代码: ```python import sys import time from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QVBoxLayout class Worker(QThread): # 定义一个信号,用于向主线程发送消息更新GUI signal = pyqtSignal(str) def __init__(self): super().__init__() def run(self): for i in range(10): time.sleep(1) self.signal.emit(f"当前进度:{i+1}/10") class MainWindow(QWidget): def __init__(self): super().__init__() self.init_ui() self.worker = Worker() self.worker.signal.connect(self.update_label) def init_ui(self): self.setWindowTitle("多线程GUI示例") self.setGeometry(200, 200, 300, 150) self.btn_start = QPushButton("开始", self) self.btn_start.clicked.connect(self.start_thread) self.label = QLabel("当前进度:0/10", self) layout = QVBoxLayout() layout.addWidget(self.btn_start) layout.addWidget(self.label) self.setLayout(layout) def start_thread(self): self.btn_start.setEnabled(False) self.worker.start() def update_label(self, msg): self.label.setText(msg) if msg == "当前进度:10/10": self.btn_start.setEnabled(True) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) ``` 这个示例程序,我们使用了PyQt5的QThread类来创建一个工作线程,该线程会休眠一秒钟,然后向主线程发送一个消息,更新GUI的一个标签。主线程有一个按钮,点击该按钮会启动工作线程。当工作线程完成时,主线程会将按钮重新激活。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值