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

简介

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

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

原本的代码

import json
import sys
import threadingimport pymysql
from PyQt5 import QtGui, QtWidgets, QtCore
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QApplication, QGraphicsDropShadowEffect, QMessageBox, QListWidgetItem
from PyQt5.QtCore import Qtfrom Assets.MessageItem import OtherMessageItem
from Assets.MessageItemMy import MyMessageItem
from ui.chat_page import Ui_Form
import websocketclass 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_idself.other_id = other_user_idself.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()  # 初始化槽函数# 在单独的线程中启动 WebSocketself.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,0shadow.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:"""passdef 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()  # 清空输入框returnQMessageBox.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_iditem = 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:"""passdef 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_posself.move(self.pos() + delta)self.click_pos = event.globalPos()def mouseReleaseEvent(self, event):self.click_pos = Noneif __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 threadingimport 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, QSizefrom 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 websocketclass NoHoverDelegate(QtWidgets.QStyledItemDelegate):"""自定义委托以禁用悬停效果"""def paint(self, painter, option, index):if option.state & QtWidgets.QStyle.State_MouseOver:option.state = option.state & ~QtWidgets.QStyle.State_MouseOversuper().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_idself.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()  # 初始化槽函数# 在单独的线程中启动 WebSocketself.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,0shadow.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:"""passdef init_history_message(self):"""# Author: Daydreamer初始化和该好友的历史聊天记录:return:"""passdef 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 将消息保存到数据库returnQMessageBox.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)returnprint(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:"""passdef 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_posself.move(self.pos() + delta)self.click_pos = event.globalPos()def mouseReleaseEvent(self, event):self.click_pos = Noneif __name__ == '__main__':app = QApplication(sys.argv)login_page = chatPage('545247018', '738053369')login_page.show()sys.exit(app.exec_())

最终效果

QQ录屏20240702164601

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

持续部署的7个陷阱及其避免方法

什么是持续部署? 持续部署是一种软件开发实践,其中代码更改会自动部署到生产中,无需开发人员或运营团队的明确批准。这实现了从开发到部署的完全自动化流程,确保新功能、错误修复和更新能够快速提供给最终用户。通过将此流程集成…

Bioconda软件安装神器:多版本并存、环境复制、环境导出

Conda包管理系统 Conda是一种通用包管理系统,旨在构建和管理任何语言的任何类型的软件。通常与Anaconda (集成了更多软件包,https://www.anaconda.com/download/#download)和Miniconda(只包含基本功能软件包, https://conda.io/miniconda.html)一起分发…

Kubernetes (K8s) 底层原理

Kubernetes (K8s) 的底层原理涉及多个关键组件和概念,确保容器化应用程序的自动化部署、扩展和管理。以下是 Kubernetes 的底层原理及其关键组件的详细描述。 核心组件 Etcd 功能:分布式键值存储,用于存储集群的所有数据,包括配置…

昇思25天学习打卡营第9天|MindSpore-Vision Transformer图像分类

Vision Transformer图像分类 Vision Transformer(ViT)简介 近些年,随着基于自注意(Self-Attention)结构的模型的发展,特别是Transformer模型的提出,极大地促进了自然语言处理模型的发展。由于Transformers的计算效率和可扩展性,它已经能够训练具有超过100B参数的空前…

【C++】运算符重载

目录 运算符重载加号运算符左移运算符递增运算符赋值运算符关系运算符函数调用运算符 运算符重载 运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型 加号运算符 目标:实现两个对象相加或类和…

java面试之jvm常见面试题

1、JVM是什么?JVM的主要组成部分? JVM(Java Virtual Machine)是Java程序的执行环境,它使得Java程序能够“一次编写,到处运行”。JVM主要由以下部分组成: 1、类加载器(ClassLoader&…

MySQL篇-SQL优化实战

SQL优化措施 通过我们日常开发的经验可以整理出以下高效SQL的守则 表主键使用自增长bigint加适当的表索引,需要强关联字段建表时就加好索引,常见的有更新时间,单号等字段减少子查询,能用表关联的方式就不用子查询,可…

某Dota/IM的某电竞对战平台玩家助手、查看战绩、胜率等

功能说明 WAR3游戏启动后,可以自动获取游戏双方的玩家列表,然后查询显示玩家的战绩及个人信息。附带查看玩家的战绩详情、最近游戏,查看对手及友方的战绩详情,据此推算出是否开黑、是否小号等信息 使用方法及运行效果 启动 查…

腾讯地图异步调用

<template><!-- 定义地图显示容器 --><div id"container"></div> </template><script setup>import { onMounted } from vue;const mapKeys import.meta.env.VITE_GLOB_TX_MAP_KEYS;function initMap() {// //定义地图中心点坐…

【MySQL备份】mysqldump基础篇

目录 1.简介 2.基本用途 3.命令格式 3.1常用选项 3.2常用命令 4.备份脚本 5.定时执行备份脚本 1.简介 mysqldump 是 MySQL 数据库管理系统的命令行实用程序&#xff0c;用于创建数据库的逻辑备份。它能够导出数据库的结构&#xff08;如表结构、视图、触发器等&#xf…

【C++题解】1456. 淘淘捡西瓜

问题&#xff1a;1456. 淘淘捡西瓜 类型&#xff1a;贪心 题目描述&#xff1a; 地上有一排西瓜&#xff0c;每个西瓜都有自己的重量。淘淘有一个包&#xff0c;包的容量是固定的&#xff0c;淘淘希望尽可能在包里装更多的西瓜&#xff08;当然要装整个的&#xff0c;不能切开…

C#——异步Task详情

C#异步Task 异步&#xff1a;多任务开始执行&#xff0c;只需要主任务 A 执行完成就算结束&#xff0c;主任务执行的时候&#xff0c;可以同时执行异步任务 B、C&#xff0c;主任务 A 可以不需要等待异步任务 B、C 的结果。 在C#中&#xff0c;异步编程主要通过async和await关…

三维地图Cesium中,如何监听地图点击事件,实现在实体上面鼠标右击时做处理。

在 Cesium 中&#xff0c;如果你想在实体&#xff08;Entity&#xff09;上实现鼠标右击&#xff08;右键点击&#xff09;的处理&#xff0c;你需要使用 Cesium 的事件系统来监听鼠标事件&#xff0c;并结合一些逻辑来判断点击是否发生在实体上。由于 Cesium 没有直接提供“点…

03-《含羞草》

含羞草 含羞草&#xff08;学名&#xff1a;Mimosa pudica Linn. &#xff09;&#xff1a;为豆科多年生草本或亚灌木&#xff0c;由于叶子会对热和光产生反应&#xff0c;受到外力触碰会立即闭合&#xff0c;所以得名含羞草。形状似绒球。开花后结荚果&#xff0c;果实呈扁圆形…

sql获取下个月开始日期与结束日期

mysql获取下个月开始日期与结束日期 SELECTid,DATE_FORMAT(DATE_ADD(LAST_DAY(limitStartTime), INTERVAL 1 DAY), %Y-%m-01) AS limitStartTime,LAST_DAY(DATE_ADD(limitStartTime, INTERVAL 1 MONTH)) AS limitEndTime,createTime,creator FROMorder_special_drugs;这里的SQ…

学习笔记——动态路由——OSPF(工作原理)

九、OSPF协议的工作原理 1、原理概要 (1)相邻路由器之间周期性发送HELLO报文&#xff0c;以便建立和维护邻居关系。 (2)建立邻居关系后&#xff0c;给邻居路由器发送数据库描述报文(DD)&#xff0c;也就是将自己链路状态数据库中的所有链路状态项目的摘要信息发送给邻居路由器…

LabVIEW汽车ECU测试系统

开发了一个基于LabVIEW开发的汽车发动机控制单元&#xff08;ECU&#xff09;测试系统。该系统使用了NI的硬件和LabVIEW软件&#xff0c;能够自动执行ECU的功能测试和性能测试&#xff0c;确保其在不同工作条件下的可靠性和功能性。通过自动化测试系统&#xff0c;大大提高了测…

使用 Rustup 管理 Rust 版本

文章目录 安装 Rustup配置镜像源安装 Rustup 安装 RustVS Code插件创建项目代码示例 Rust 官网&#xff1a;https://www.rust-lang.org/zh-CN/Crates 包管理&#xff1a;https://crates.io/Rust 程序设计语言&#xff1a;https://kaisery.github.io/trpl-zh-cn/通过例子学 Rust…

Jdk17是否有可能代替 Jdk8

JDK发展历史和开源 2006年SUN公司开源JDK&#xff0c;成立OpenJDK组织。2009年Oracle收购SUN&#xff0c;加快JDK发布周期。Oracle JDK与OpenJDK功能基本一致&#xff0c;但Oracle JDK提供更长时间的更新支持。 JDK版本特性 JDK11是长期支持版本&#xff08;LTS&#xff09;…

DevOps认证是什么?DevOps工具介绍

DevOps 这个词是由Development&#xff08;开发&#xff09; 和 Operations&#xff08;运维&#xff09;组合起来的&#xff0c;你可以把它理解成为一种让开发团队和运维团队紧密合作的方法。 DevOps从2009年诞生到现在已经14年多了&#xff0c;一开始大家还在摸索&#xff0…