I'm creating a chat client and chat server after a several month programming hiatus. The goal is to make one with features I want but it's also a learning project. Although it's mostly because I'm annoyed at cough skype.
I've realised I don't know if anything I've written currently is any good (never had anyone critique my code) so I'd like to know if the structure of the program is any good (especially how I handle tkinter and classes) and my use of control flow etc.
The code currently has 2 classes, one that contains the GUI elements (tkinter) and the other handles the networking side (threading and socket). It creates a thread for the listen method so it is constantly awaiting data from the server. Whilst the initial thread/process is used for the tkinter mainloop.
You can get/see the tkHyperlinkManager file here and the server here
try: #if python3
from tkinter import *
import tkinter.font as tkFont
except: #if python2
from Tkinter import *
import tkFont
from socket import *
from threading import *
import time,sys, json,tkHyperlinkManager, webbrowser #tkHyperlinkManager is another file, I didn't write it.
class chatGUI: #Class That Handles GUI related tasks.
def __init__(self):#Initializing window settings
self.bgColour = "#2a2a2a" #First COlour was: "#607D8B"
window.title("Oke's Chat Client")
window.configure(bg=self.bgColour)
self.chat = Frame(window,bg=self.bgColour)
self.menu = Frame(window,bg=self.bgColour)
self.checkFile() #Check for options/settings configuration
menuBar = Menu(window,foreground=self.bgColour,activeforeground=self.bgColour)
menuBar.add_command(label="Options", command=self.optionMenu)
window.config(menu=menuBar)
self.chatFrame()#Loads Chat GUI into memory.
#Load the first Menu
self.menuFrame()#Loads the MENU GUI into memory.
self.menu.place(y=0,x=0,height=500,width=500)
self.hyperList = ["http://","www.","https://","ftp://"]#Contains hyperlink triggers.
def menuFrame(self):#Load in all widgets on main menu
#BG IMAGE
bgImg = PhotoImage(file="Images/bgImg.gif")
bgLabel = Label(self.menu,image=bgImg,bg=self.bgColour)
bgLabel.image=bgImg
bgLabel.place(x=0,y=0)
#Error
self.Error = Label(self.menu,text="Unable to connect to server\n")
#Label Msg
labelImg = PhotoImage(file="Images/menu_text.gif")
label = Label(self.menu,image=labelImg,bg=self.bgColour)
label.image = labelImg
label.place(y=75,x=100)
#Entry Widget
self.aliasEntry = Entry(self.menu)
self.aliasEntry.place(y=100,x=190)
#Connect Button
buttonImg =PhotoImage(file="Images/buttonImg.gif")
button = Button(self.menu,text="Connect",command=self.switchToChat,image=buttonImg,bg=self.bgColour,borderwidth=0)
button.image = buttonImg
button.place(y=235,x=200)
#Window Settings
window.resizable(width=FALSE, height=FALSE)
window.minsize(500,500)
def chatFrame(self):#Load in all chat GUI widgets and related objects
#Set up fonts and StringVars
bold_font = tkFont.Font(family="Helvetica",size=10,weight="bold")
norm_font = tkFont.Font()
sv= StringVar() #stringVar to hold string from Entry Widget
sv.trace("w", lambda name, index, mode, sv=sv: self.checkEntry(sv)) #Calls self.checkEntry when ever entryStr String Var is changed.
#Scrollbar and the chatText widgets
self.scrollBar = Scrollbar(self.chat)
self.scrollBar.grid(column=1,row=0,sticky=N+S+E+W)
self.mainText = Text(self.chat, wrap=WORD,bg=self.bgColour,state=DISABLED,yscrollcommand=self.scrollBar.set)
self.mainText.grid(column=0,row=0,sticky=N+S+E+W)
self.mainText.tag_configure("bold", font=bold_font, foreground="#7dbcc1")
self.mainText.tag_configure("normal", font=norm_font, foreground ="white")
self.scrollBar.config(command=self.mainText.yview)
#Userbar
self.userBar = Text(self.chat,width=20,bg=self.bgColour,fg="white",state=DISABLED)
self.userBar.grid(column=2, row=0,sticky=N+S+E+W)
self.userBar.tag_configure("bold", font=bold_font, foreground="#7dbcc1")
#TextEntry
self.textEntry = Entry(self.chat,textvariable=sv,bg =self.bgColour,fg="white")#Note, textvar set to entryStr, text entered is stored in that var.
self.textEntry.grid(row=1,column=0,sticky=N+S+E+W,rowspan=2)
#make hyperlink class
self.hyperlink = tkHyperlinkManager.HyperlinkManager(self.mainText)
#Send button
Button(self.chat, text="Send",command= lambda: loadNet.speak(self.alias, self.textEntry),bg=self.bgColour,fg="white").grid(column=2,row=1,sticky=NW)
#Grid Geometry Config to make only chat box resizable.
self.chat.columnconfigure(0,weight=1)
self.chat.columnconfigure(2,minsize=140)
self.chat.rowconfigure(0,weight=1)
def optionMenu(self):#Load in all widgets on option Menu
self.timeStamp = IntVar()
self.hourStamp = IntVar()
self.timeStamp.set(self.optionData["timeStamp"])
self.hourStamp.set(self.optionData["timeSet"])
self.optionWindow = Toplevel(bg=self.bgColour,height=200,width=200)
self.optionWindow.title("ChatClient Options")
Checkbutton(self.optionWindow, text="TimeStamp", variable=self.timeStamp).pack()
Checkbutton(self.optionWindow, text="Use 24 Hour timestamp", variable=self.hourStamp).pack()
Button(self.optionWindow,text="Apply", command=self.saveSettings).pack()
def switchToChat(self): #handles closing menu and switching to chat and calls connect function
self.alias = self.aliasEntry.get() #Grabs alias entered at menu
if self.alias.isspace() == False and self.alias != "" and len(self.alias) < 16:#make sure name is under 16 chars and isnt whitespace.
try:#Try Connect to Server.
loadNet.connect(self.alias)#Replace this line with pass if you wish to run without a server.
except:#Unpack chat, repack menu.
print("Unable to connect to server")
self.chat.pack_forget()
self.menu.place(y=0,x=0,height=500,width=500)
self.Error.pack()
else:#Called only if try works, if it connects.
window.bind("<Return>", lambda event: loadNet.speak(self.alias, self.textEntry))
self.menu.place_forget() #Remove menu
self.chat.pack(fill=BOTH, expand=YES)#put chat GUI in.
window.resizable(width=TRUE, height=TRUE)
window.minsize(500,410)
def checkFile(self): #Function handles reading in the settings from txt file, if try fails it meants it's unreadable or doesn't exist and makes a new file.
try:
optionFile = open("options.txt")
self.optionData = json.loads(optionFile.read())
except:
print("Options Configuration File Missing. Or Unreadable. \n Creating new file...")
optionFile = open("options.txt","w+")
optionFile.truncate()
Dict = {
"timeStamp": 1,
"timeSet": 1
}
Dict = json.dumps(Dict)
optionFile.write(Dict)
optionFile.close()
optionFile = open("options.txt")
self.optionData = json.loads(optionFile.read())
optionFile.close()
def saveSettings(self): #Save setting vars to txt file. Called by Apply button push.
self.optionData["timeStamp"] = self.timeStamp.get()
self.optionData["timeSet"] = self.hourStamp.get()
optionFile = open("options.txt","w+")
optionFile.truncate()
optionFile.write(json.dumps(self.optionData))
optionFile.close()
def checkEntry(self, entryStr): #checks the text entry,sets the contents of Entry widget to max of 1000 chars, called when ever text is entered.
c = entryStr.get()[0:1000]
entryStr.set(c)
def displayData(self,data):#Handles displaying message data.
print(data)
self.mainText.config(state=NORMAL)
if self.optionData["timeStamp"]:#if using timestamp
if self.optionData["timeSet"]:#if using 24HR
self.mainText.insert(END, time.strftime('%H:%M', time.localtime()) + " - "+str(data[0]), 'bold')#24 hour
else:
self.mainText.insert(END, time.strftime('%I:%M', time.localtime()) + " - "+str(data[0]), 'bold')#12 HOUR
else:#No timestamp
self.mainText.insert(END, str(data[0]), 'bold')#No TimeStamp
msgSplit = data[1].split()#Split message up for analysis of hyperlinks
for msgDat in msgSplit:#Check message for hyperlinks.
for hyperID in self.hyperList:# check for different types of hyper link triggers
if msgDat.startswith(hyperID):
self.mainText.insert(END, str(msgDat), self.hyperlink.add(lambda:webbrowser.open(msgDat)))#Make the message a hyperlink that opens webbrowser
self.mainText.insert(END, " ")
break
else:
self.mainText.insert(END, str(msgDat)+" ", "normal")
self.mainText.insert(END, "\n")
self.mainText.config(state=DISABLED)
self.mainText.see("end") #Makes view of text box at bottom so you don't have to scroll down. Looking at you skype >:(
def modUserBar(self,userList):#Called when someone connects/disconnects. Modifies the userbar which is a text widget.
self.userBar.config(state=NORMAL)
self.userBar.delete(1.0, END)#Empty it out
for user in userList:
self.userBar.insert(END, str(user)+"\n", 'bold')
self.userBar.config(state=DISABLED)
class netMan:#Handles network related part of program
def __init__(self):
self.s = socket(AF_INET, SOCK_STREAM)#Make socket object
def connect(self,alias):
self.s.connect(('192.168.1.97',11116))
self.s.send(alias.encode('utf-8'))#Send alias
listenThread = Thread(target=self.listen)#Define the listening thread, required because window.mainloop :(
listenThread.start()#Start the thread!
def listen(self):#Loop forever in thread
while True:
try:
data = self.s.recv(3000)
except:
print("Disconnected from server")
break
try:
dataDecode = json.loads(data.decode('utf-8'))
except:
print("json decoding error\n")
else:
if dataDecode[0] == 1:
loadGUI.displayData(dataDecode[1:])
elif dataDecode[0] == 0:
loadGUI.modUserBar(dataDecode[1])
self.s.close()
def speak(self, alias, textEntry, event=None): #Called when ever send button pushed or Enter is pressed
msg = textEntry.get()
if msg != "": #no whitespace!
packet= json.dumps([1,alias+": ",msg],ensure_ascii=False)
textEntry.delete(0,END)
try:
self.s.send(packet.encode('utf-8'))
except:
print("unable to reach server...?\n")
window = Tk()
loadNet = netMan()
loadGUI = chatGUI()
window.mainloop()