Python Tkinter如何同时运行两个while循环实现单窗口聊天功能
Tkinter整合双端聊天系统实现方案
核心注意事项
原脚本的阻塞式while True循环+input()逻辑不能直接放到Tkinter代码中,否则会直接阻塞UI主线程导致窗口无响应,实现时遵循以下规则即可:
- 公共配置、加密工具、请求会话全局初始化,避免重复加载
- 5秒间隔的消息拉取逻辑放到守护子线程运行,和UI线程隔离
- 子线程不可直接操作Tkinter控件,通过Tkinter自带的
after()方法调度UI更新,规避线程安全问题 - 替换原命令行
input()为Tkinter输入控件,绑定按钮/回车事件触发消息发送 - 增加全局退出标志位,窗口关闭时自动终止轮询循环、发送离会消息
整合后完整代码
首先修正原代码里的HTML转义错误(原代码中&转义为&、<<<>>>转义为<<<>>>,修复缩进问题),整合代码如下:
import tkinter as tk from tkinter import scrolledtext import threading import datetime import json from time import sleep from datetime import datetime import requests from cryptography.fernet import Fernet from pytz import timezone, utc import configme as con # -------------------------- 全局配置初始化 -------------------------- nam = con.my_name cookies_dict = con.cookie key = con.crypto_key send_url = con.base_url + '/config.php' log_url = con.base_url + '/log.json' poll_interval = con.receive_time user_timezone = timezone(con.my_timezone) time_format = con.date_time_format # 加密实例 fernet = Fernet(key) # 请求会话复用 req_session = requests.Session() # 已读消息偏移量 msg_offset = 0 # 线程退出标志 exit_flag = False # -------------------------- 工具函数 -------------------------- def make_msg_id(): return datetime.datetime.now().timestamp() def format_localtime(timestamp): stamptime = int(float(timestamp)) gmt0_time = utc.localize(datetime.utcfromtimestamp(stamptime)) return gmt0_time.astimezone(user_timezone).strftime(time_format) def send_leave_msg(): """发送离会系统消息""" req_session.get( send_url, params={ 'iD': make_msg_id(), 'name': '<<<>>>', 'msg': f"{nam} has left the room." }, cookies=cookies_dict ) # -------------------------- UI逻辑 -------------------------- root = tk.Tk() root.title("简易聊天客户端") root.geometry("750x550") # 消息显示区(只读) msg_box = scrolledtext.ScrolledText(root, wrap=tk.WORD, state='disabled') msg_box.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) # 底部输入区 input_frame = tk.Frame(root) input_frame.pack(padx=10, pady=(0,10), fill=tk.X) msg_input = tk.Entry(input_frame) msg_input.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,5)) def send_msg(event=None): """发送消息逻辑,绑定回车和发送按钮""" content = msg_input.get().strip() msg_input.delete(0, tk.END) if not content: return if content == ".exit": on_window_close() return # 加密发送 enc_msg = fernet.encrypt(content.encode()) req_session.get( send_url, params={ 'iD': make_msg_id(), 'name': nam, 'msg': enc_msg }, cookies=cookies_dict ) send_btn = tk.Button(input_frame, text="发送", width=8, command=send_msg) send_btn.pack(side=tk.RIGHT) # 绑定回车发送 msg_input.bind('<Return>', send_msg) def update_msg_box(new_msg): """线程安全更新消息框""" msg_box.config(state='normal') msg_box.insert(tk.END, new_msg + "\n") msg_box.config(state='disabled') msg_box.see(tk.END) # -------------------------- 消息轮询子线程 -------------------------- def poll_msg_loop(): global msg_offset while not exit_flag: try: resp = req_session.get(log_url, cookies=cookies_dict, timeout=10) msg_list = json.loads(resp.text) total_msg = len(msg_list) # 首次加载处理 if msg_offset == 0 and total_msg == 0: root.after(0, update_msg_box, "当前聊天室暂无历史消息") msg_offset = total_msg sleep(poll_interval) continue # 遍历新消息 for msg in msg_list[msg_offset:]: local_time = format_localtime(msg['id']) if msg['nam'] == '<<<>>>': # 系统消息 show_text = f"{local_time} :: 系统提示 :: {msg['msg']}" else: # 普通消息解密 dec_msg = fernet.decrypt(bytes(msg['msg'], "utf-8")).decode() show_text = f"{local_time} :: {msg['nam']} :: {dec_msg}" # 调度到主线程更新UI root.after(0, update_msg_box, show_text) msg_offset = total_msg except Exception as e: root.after(0, update_msg_box, f"[拉取消息出错] {str(e)}") sleep(poll_interval) def on_window_close(): """窗口关闭回调""" global exit_flag exit_flag = True try: send_leave_msg() except: pass root.destroy() root.protocol("WM_DELETE_WINDOW", on_window_close) # -------------------------- 启动逻辑 -------------------------- if __name__ == "__main__": # 发送入会消息 if len(nam.strip()) > 0: req_session.get( send_url, params={ 'iD': make_msg_id(), 'name': '<<<>>>', 'msg': f"{nam} join the room." }, cookies=cookies_dict ) # 启动轮询线程 poll_thread = threading.Thread(target=poll_msg_loop, daemon=True) poll_thread.start() # 启动UI主循环 root.mainloop()
使用说明
- 确保原
configme.py配置文件和整合后的脚本放在同一目录下,依赖库(requests、cryptography、pytz)提前安装 - 输入框输入内容按回车或者点发送按钮即可发消息,输入
.exit即可退出聊天室 - 消息拉取间隔沿用原配置文件里的
receive_time参数,不需要额外修改
内容的提问来源于stack exchange,提问作者Zahid Hossen
相关产品推荐
相关产品推荐

