模拟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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/766166.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

llama-factory训练RLHF-PPO模型

理论上RLHF(强化学习)效果比sft好,也更难训练。ppo有采用阶段,步骤比较多,训练速度很慢. 记录下工作中使用llama-factory调试rlhf-ppo算法流程及参数配置,希望对大家有所帮助. llama-factory版本: 0.8.2 一 rlhf流程 ppo训练流程图如下, 会…

【Linux】—Xshell、Xftp安装

文章目录 前言一、下载Xshell、Xftp二、安装Xshell三、使用XShell连接Linux服务器四、修改windows的主机映射文件(hosts文件)五、远程连接hadoop102/hadoop103/hadoop104服务器六、安装Xftp 前言 XShell远程管理工具,可以在Windows界面下来访…

Springboot整合RedisTemplate以及业务工具类示例

docker安装Redis参考我另一篇博客Docker安装Redis及持久化 一、Get-Started 依赖 <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis --> <dependency><groupId>org.springframework.boot</groupId>…

Java_多线程:线程池

1、线程池优点&#xff1a; 降低资源消耗&#xff1a;通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度&#xff1a;当任务到达时&#xff0c;任务可以不需要等到线程创建就能立即执行。提高线程的可管理性&#xff1a;线程是稀缺资源&#xff0c;如果无限…

Django 多对多关系

多对多关系作用 Django 中&#xff0c;多对多关系模型的作用主要是为了表示两个模型之间的多对多关系。具体来说&#xff0c;多对多关系允许一个模型的实例与另一个模型的多个实例相关联&#xff0c;反之亦然。这在很多实际应用场景中非常有用&#xff0c;比如&#xff1a; 博…

因版本冲突导致logback的debug日志不打印

因框架调整&#xff0c;降级了logback的版本号&#xff0c;由1.3.12降级为1.2.11&#xff08;因框架限制&#xff0c;只能采用1.2版本&#xff09;&#xff0c;降级后发现debug日志无法打印出来&#xff0c;logback.xml配置文件不生效。后排查发现是与slf4j的版本兼容问题 依赖…

搜维尔科技:数据手套为什么要选择SenseGlove

了解 SenseGlove SenseGlove 是一支由电子工程师、触觉研究人员和计算机视觉专家、XR 开发人员、UX 设计师和产品创新者组成的科幻爱好者团队&#xff0c;他们拥有丰富人类能力和赋予 Metaverse 意义的技能和热情。 推进触觉技术是我们实现这一目标的方式。 公司及产品背景 S…

基于Hadoop平台的电信客服数据的处理与分析③项目开发:搭建Kafka大数据运算环境---任务12:安装Kafka

任务描述 任务内容为安装和配置Kafka集群。 任务指导 Kafka是大数据生态圈中常用的消息队列框架 具体安装步骤如下&#xff1a; 1. 解压缩Kafka的压缩包 2. 配置Kafka的环境变量 3. 修改Kafka的配置文件&#xff0c;Kafka的配置文件存放在Kafka安装目录下的config中 4. 验证…

【融合ChatGPT等AI模型】Python-GEE遥感云大数据分析、管理与可视化及多领域案例应用

随着航空、航天、近地空间遥感平台的持续发展&#xff0c;遥感技术近年来取得显著进步。遥感数据的空间、时间、光谱分辨率及数据量均大幅提升&#xff0c;呈现出大数据特征。这为相关研究带来了新机遇&#xff0c;但同时也带来巨大挑战。传统的工作站和服务器已无法满足大区域…

JDK动态代理-AOP编程

AOPTest.java&#xff0c;相当于main函数&#xff0c;经过代理工厂出来的Hello类对象就不一样了&#xff0c;这是Proxy.newProxyInstance返回的对象&#xff0c;会hello.addUser会替换为invoke函数&#xff0c;比如这里的hello.addUser("sun", "13434");会…

【驱动篇】龙芯LS2K0300之红外驱动

实验目标 编写HX1838红外接收器驱动&#xff0c;根据接收的波形脉冲解码红外按键键值 模块连接 模块连接&#xff1a;VCC接Pin 2&#xff0c;GND接Pin1&#xff0c;DATA接Pin16 驱动代码 HX1838 GPIO初始化&#xff0c;申请中断&#xff0c;注意&#xff1a;GPIO48默认是给…

vscode语言模式

1.背景 写vue3ts项目的时候&#xff0c;用到了volar插件&#xff0c;在单文件使用的时候&#xff0c;鼠标悬浮在代码上面会有智能提示&#xff1b; 但是最近volar插件提示被弃用了&#xff0c;然后我按照它的官方提示&#xff0c;安装了Vue-official扩展插件&#xff0c;但是…

Vue3 特点以及优势-源码解剖

Vue3 特点以及优势-Vue3.4源码解剖 Vue3 特点以及优势 1.声明式框架 命令式和声明式区别 早在 JQ 的时代编写的代码都是命令式的&#xff0c;命令式框架重要特点就是关注过程声明式框架更加关注结果。命令式的代码封装到了 Vuejs 中&#xff0c;过程靠 vuejs 来实现 声明式代…

剑神诀_单机架设_无需虚拟机_小白专用

前言 今天给大家带来一款单机游戏的架设&#xff1a;剑神诀&#xff0c;一键端 无需虚拟机 如今市面上的资源参差不齐&#xff0c;大部分的都不能运行&#xff0c;本人亲自测试&#xff0c;运行视频如下&#xff1a; 剑神诀 搭建教程 此游戏架设不需要安装虚拟机&#xff0c;…

爬虫cookie是什么意思

“爬虫 cookie”指的是网络爬虫在访问网站时所使用的cookie&#xff0c;网络爬虫是一种自动化程序&#xff0c;用于在互联网上收集信息并进行索引&#xff0c;这些信息可以用于搜索引擎、数据分析或其他目的。 本教程操作系统&#xff1a;Windows10系统、Dell G3电脑。 “爬虫…

SpringBoot 项目整合 MyBatisPlus 框架,附带测试示例

文章目录 一、创建 SpringBoot 项目二、添加 MyBatisPlus 依赖三、项目结构和数据库表结构四、项目代码1、application.yml2、TestController3、TbUser4、TbUserMapper5、TestServiceImpl6、TestService7、TestApplication8、TbUserMapper.xml9、MyBatisPlusTest 五、浏览器测试…

新鲜出炉!恭喜这 5 位同学中选 NebulaGraph 社区 2024 开源之夏项目!

开源之夏是中国科学院软件研究所发起的“开源软件供应链点亮计划”系列暑期活动&#xff0c;旨在鼓励高校学生积极参与开源软件的开发维护&#xff0c;促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区&#xff0c;针对重要开源软件的开发与维护提供项目开发任务&#xf…

stm32学习笔记---USART串口外设(理论部分)

目录 USART简介 USART的框图 串口的引脚 USART的基本结构 数据帧 起始位侦测 数据采样 波特率发生器 USD转串口模块的原理图 声明&#xff1a;本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记&#xff0c;我之所以记录下来是为了方便自己日后复习。如果你…

个人微信二次开发

​ 由于自身在机器人方面滚爬多年&#xff0c;现在收藏几个宝藏机器人 推荐一下自己常用的机器人&#xff1a; 适合有技术开发的公司&#xff0c;可以自主开发所需要的功能&#xff01;十分齐全 测试问文档&#xff1a;https://www.wkteam.cn/ 有需要的兄弟可以看一下&#…

手写一个基于SpringBoot的MVC架构,默认实现CRUD和导入导出功能

文章目录 前言正文一、项目结构二、技术点三、部分核心代码3.1 core-tool 中的核心代码3.1.1 所有实体的通用父类 SuperEntity3.1.2 所有枚举的父接口 BaseEnum3.1.3 所有业务异常的父接口 BaseException 3.2 mvc-tool 中的核心代码3.2.1 CrudController 接口定义3.2.2 默认的C…
最新文章