Tkinter线程化:如何脱离按钮触发实现函数的线程调用?
问题与解决方案
问题描述
我开发了一套客户端-服务器程序,服务器接收客户端每次点击按钮发来的计数器值,通过该计数器计算出code值,需要将code与输入框e中的内容进行比较:若两者一致,GUI显示"Congratulations: You have been granted access!";若不一致,则显示"Your access has been denied"。
当前存在的问题
- 必须使用Threading避免GUI冻结,但如果在按钮外启动线程,GUI无法加载;
- 若在按钮中启动线程,比较逻辑会自动执行(无需点击按钮),但我需要点击按钮才执行比较操作,且该比较需支持多次执行。
问题根源
- 按钮绑定错误:原代码中
command=threading.Thread(target=myClick).start()会直接执行start()方法,导致线程在按钮创建时就启动,而非点击按钮时触发。正确的做法是绑定一个函数,在函数内部启动线程。 - Socket监听逻辑与比较逻辑耦合:原
myClick函数同时包含了socket接收客户端数据和比较输入的逻辑,导致点击按钮的逻辑被socket阻塞逻辑覆盖,无法实现手动触发比较。 - 线程安全问题:直接在子线程中操作Tkinter组件会引发线程安全问题,Tkinter要求所有GUI操作必须在主线程中执行。
修改后的代码
服务器代码
import socket import tkinter as tk import hmac import hashlib import threading HOST = '127.0.0.1' PORT = 6595 root = tk.Tk() root.title("Server") root.geometry("300x200") e = tk.Entry(root, width=50, bg="white", fg="black") e.pack() # Socket相关变量 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((HOST, PORT)) server.listen() communication_socket = None current_code = "" # 存储最新计算出的code值 key = bytes("800070FF00FF08012", 'utf-8') def socket_listen(): """在后台线程中监听客户端连接并接收计数器值""" global communication_socket, current_code, key while True: try: communication_socket, address = server.accept() while True: counter = communication_socket.recv(1024) if not counter: break counter = counter.decode('utf-8') msg = bytes(f'{counter}', 'utf-8') digest = hmac.new(key, msg, "sha256").digest() offset = digest[19] & 0xF code = digest[offset: offset + 4] code = int.from_bytes(code, "big") & 0x7FFFFFFF code = code % 1000000 code = "{:06d}".format(code) key = digest # 更新key,无需replace,直接赋值即可 # 使用after在主线程更新GUI组件 root.after(0, lambda c=counter, cd=code: update_received_info(c, cd)) # 更新全局的current_code current_code = code except Exception as e: print(f"连接异常: {e}") break def update_received_info(counter, code): """在主线程更新接收信息的标签""" myLabel.config(text=f"Message from client is {counter}") Hash.config(text=code) def compare_code(): """点击按钮时执行的比较逻辑""" input_val = e.get().strip() if current_code == input_val: result.config(text="Congratulations: You have been granted access!") else: result.config(text="Your access has been denied") # 启动后台监听线程 threading.Thread(target=socket_listen, daemon=True).start() # 创建按钮,绑定比较函数 myButton = tk.Button(root, text="Compare Input to Code", command=compare_code) myButton.pack() myLabel = tk.Label(root) myLabel.pack() Hash = tk.Label(root) Hash.pack() result = tk.Label(root) result.pack() root.mainloop()
客户端代码
import tkinter as tk import hmac import hashlib import socket import threading HOST = '127.0.0.1' PORT = 6595 cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM) root = tk.Tk() root.title("Client") root.geometry("200x100") # 把连接操作放到线程中,避免阻塞GUI加载 def connect_to_server(): try: cli.connect((HOST, PORT)) except Exception as e: print(f"连接服务器失败: {e}") threading.Thread(target=connect_to_server, daemon=True).start() key = bytes("800070FF00FF08012", 'utf-8') x = 0 def button_click(): global x, key x += 1 msg = bytes(f'{x}', 'utf-8') digest = hmac.new(key, msg, "sha256").digest() key = digest # 直接更新key offset = digest[19] & 0xF code = digest[offset : offset + 4] code = int.from_bytes(code, "big") & 0x7FFFFFFF code = code % 1000000 code = "{:06d}".format(code) myLabel.config(text=code) counter = str(x) try: cli.send(counter.encode('utf-8')) except Exception as e: print(f"发送数据失败: {e}") count.config(text=counter) OTP = tk.Button(root, text="Generate OTP", padx=20, pady=10, command=button_click) OTP.pack() myLabel = tk.Label(root) myLabel.pack() count = tk.Label(root) count.pack() root.mainloop()
修改说明
- 分离Socket监听与比较逻辑:
- 将socket监听逻辑放到独立的
socket_listen函数中,在后台线程启动,避免阻塞GUI。 - 使用全局变量
current_code存储最新计算出的code值,供比较函数使用。
- 将socket监听逻辑放到独立的
- 修复按钮绑定问题:
- 按钮的
command直接绑定compare_code函数,点击按钮时才触发比较操作。
- 按钮的
- 线程安全处理GUI更新:
- 使用Tkinter的
after方法在主线程中更新GUI组件,避免子线程直接操作组件引发的线程安全问题。
- 使用Tkinter的
- 优化key更新逻辑:直接将
key赋值为新的digest,替代原有的replace操作,更简洁高效。 - 客户端连接优化:将服务器连接操作放到后台线程,避免阻塞GUI加载。
内容的提问来源于stack exchange,提问作者Anthony J. B.
相关产品推荐
相关产品推荐

