I am making a client-server system using tkinter for the GUI, and I need to pass the app variable through the thread which communicates with the server. Problem is, I also need to pass the thread through the app.
The following is what I've tried so far, but I simply have no idea what to do in this situation. If my way of doing this is just blatantly incorrect, please correct me. I'm not a pro so I may be looking at it in the wrong way completely.
import tkinter as tk
import socket
import sys
from tkinter.simpledialog import askstring
from tkinter import *
from tkinter import messagebox
import time
import threading
import protocol
IP = "127.0.0.1"
PORT = 1234
BIG_BUFFER = 256
stop_event = threading.Event()
logged_in = False
def packed(cmd):
return cmd.encode()
def receive_data_from_server(client, app):
while not stop_event.isSet():
try:
server_cmd = client.recv(BIG_BUFFER).decode()
if server_cmd:
print(server_cmd)
if protocol.check_cmd(server_cmd):
app.handle_server_response(server_cmd)
else:
print("invalid cmd: " + server_cmd)
except Exception as err:
print(err)
def handle_server_response(self, response):
if response == "log_in_acc":
self.controller.login_successful()
elif response == "log_in_err":
self.error_label.config(text="Invalid username or password")
else:
print("Unknown response from server:", response)
class LogInWindow(tk.Tk):
def __init__(self, client, data_thread, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = client
self.data_thread = data_thread
self.title("Log In")
self.geometry("500x400")
self.protocol("WM_DELETE_WINDOW", self.on_closing)
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(1, weight=1)
self.frames = {}
for F in (LoginPage, RegisterPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=1, sticky="nsew")
# Initially show LoginPage
self.show_frame(LoginPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
def login_successful(self):
# Destroy the current main application
self.destroy()
# Create and start a new application
global logged_in
logged_in = True
def on_closing(self):
if messagebox.askokcancel("Quit", "Do you want to close the application?"):
self.client.send(packed("exit"))
stop_event.set()
self.destroy()
self.data_thread.join()
def send(self, msg):
self.client.send(packed(msg))
class LoginPage(tk.Frame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
label = tk.Label(self, text="Login Page", font=("Helvetica", 18))
label.pack(pady=10, padx=10)
username_label = tk.Label(self, text="Username:")
username_label.pack()
self.username_entry = tk.Entry(self)
self.username_entry.pack()
password_label = tk.Label(self, text="Password:")
password_label.pack()
self.password_entry = tk.Entry(self, show="*")
self.password_entry.pack()
login_button = tk.Button(self, text="Login", command=self.login)
login_button.pack(pady=5)
register_button = tk.Button(self, text="Register", command=lambda: controller.show_frame(RegisterPage))
register_button.pack(pady=5)
self.error_label = tk.Label(self, text="", fg="red")
self.error_label.pack(pady=5)
def login(self):
username = self.username_entry.get()
password = self.password_entry.get()
self.controller.send("log_in-" + username + "-" + password)
if username == "user" and password == "password": # todo send log in request and receive
self.controller.login_successful()
else:
self.error_label.config(text="Invalid username or password")
def main():
try:
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((IP, PORT)) # connect to server
except Exception as error:
print(error) # could be an error connecting to server or establishing the socket
data_thread = threading.Thread(target=receive_data_from_server,
args=(client, app))
data_thread.daemon = True
data_thread.start()
app = LogInWindow(client, data_thread)
data_thread.app = app
if not stop_event.isSet():
app.mainloop()
time.sleep(0.1)
client.close()
if __name__ == "__main__":
main()
Pass the app variable to the thread after it's created, rather than trying to pass it during thread creation, which won't work due to the order of operations.
Use a lock to ensure that the app variable is safely accessed and modified by both the main thread and the data thread. Here's the modified code:
import tkinter as tk
import socket
import sys
from tkinter.simpledialog import askstring
from tkinter import *
from tkinter import messagebox
import time
import threading
import protocol
IP = "127.0.0.1"
PORT = 1234
BIG_BUFFER = 256
stop_event = threading.Event()
logged_in = False
def packed(cmd):
return cmd.encode()
def receive_data_from_server(client, app):
while not stop_event.isSet():
try:
server_cmd = client.recv(BIG_BUFFER).decode()
if server_cmd:
print(server_cmd)
if protocol.check_cmd(server_cmd):
app.handle_server_response(server_cmd)
else:
print("invalid cmd: " + server_cmd)
except Exception as err:
print(err)
class LogInWindow(tk.Tk):
def __init__(self, client, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lock = threading.Lock() # Lock for accessing app variable
self.client = client
self.title("Log In")
self.geometry("500x400")
self.protocol("WM_DELETE_WINDOW", self.on_closing)
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(1, weight=1)
self.frames = {}
for F in (LoginPage, RegisterPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=1, sticky="nsew")
# Initially show LoginPage
self.show_frame(LoginPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
def login_successful(self):
# Destroy the current main application
self.destroy()
# Create and start a new application
global logged_in
logged_in = True
def on_closing(self):
if messagebox.askokcancel("Quit", "Do you want to close the application?"):
self.client.send(packed("exit"))
stop_event.set()
self.destroy()
def send(self, msg):
self.client.send(packed(msg))
class LoginPage(tk.Frame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
label = tk.Label(self, text="Login Page", font=("Helvetica", 18))
label.pack(pady=10, padx=10)
username_label = tk.Label(self, text="Username:")
username_label.pack()
self.username_entry = tk.Entry(self)
self.username_entry.pack()
password_label = tk.Label(self, text="Password:")
password_label.pack()
self.password_entry = tk.Entry(self, show="*")
self.password_entry.pack()
login_button = tk.Button(self, text="Login", command=self.login)
login_button.pack(pady=5)
register_button = tk.Button(self, text="Register", command=lambda: controller.show_frame(RegisterPage))
register_button.pack(pady=5)
self.error_label = tk.Label(self, text="", fg="red")
self.error_label.pack(pady=5)
def login(self):
username = self.username_entry.get()
password = self.password_entry.get()
self.controller.send("log_in-" + username + "-" + password)
if username == "user" and password == "password": # todo send log in request and receive
self.controller.login_successful()
else:
self.error_label.config(text="Invalid username or password")
def main():
try:
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((IP, PORT)) # connect to server
except Exception as error:
print(error) # could be an error connecting to server or establishing the socket
app = LogInWindow(client)
data_thread = threading.Thread(target=receive_data_from_server, args=(client, app))
data_thread.daemon = True
data_thread.start()
app.mainloop()
stop_event.set() # Signal the data thread to stop
data_thread.join() # Wait for the data thread to finish
client.close()
if __name__ == "__main__":
main()
This way, the app variable is passed to the receive_data_from_server thread after the app object is fully initialized. Also, a lock is used to ensure safe access to the app variable from both the main thread and the data thread.