dn <
[email protected]> writes:
* which may say something unfortunate about my coding/design, or may
simply show that SODD (Stack Overflow Driven Development - or the GitHub >equivalent) leaves much to be desired. Hence the gold-plated advice:
never copy-paste code without understanding it first!
I wanted to write a chat program for my classroom with a GUI
in tkinter. I never really learned tkinter, but using Web
search engines and learning from programming Q&A sites and other
Web publications, I was able to make it work in just a few hours.
I am very grateful for the Web search engines and the Web sites
to help me do this! I can still remember how much harder
programming was in the pre-Web days.
I had no time for learning tkinter properly nor reviewing the
code. But now, later, I indeed found the time to read a tutorial
about tkinter and also review and refactor the code of my chat
program (the refactoring now is a work in progress).
The chat program allows people to chat with each other who
may not be able to exchange data via TCP/IP but can share
a directory as in a LAN. One needs to enter that directory
into the source code of the chat server (see below), then
everyone starts one instance on his machine and then people
can exchange chat messages via a file (I think of it as a
"hidden file") in that common LAN directory.
You also could use it as a note logger on a single machine
as it will label each new entry with a time stamp. You could
run two instances on a single machine to see how they share
messages.
Feel free to try to run my LAN chat program after configuring
it via the configuration section in the code below!
# first line
#
# License: Copyright (c) 2022 Stefan Ram <
[email protected]>.
# You may not redistribute modified versions of this program,
# but you may use this program as is, modify it and use the
# modifed version, or you may redistribute this unmodified
# version from "first line" to "last line", and there's no warranty.
#
# User Manual:
# Type the message into the text field in the bottom.
# It may be a multi-line message.
# Use the "send" button or press Ctrl-Enter to send the message.
# To exit, send "exit" or close the window.
# To configure, see the code section just below the imports.
import datetime
import io
import math
import os
import pathlib
import re
import socket
import sys
import threading
import time
import tkinter
import tkinter.messagebox
import tkinter.font
import tkinter.scrolledtext
#u to configure this app, you can make changes in the following section:
#u *** Begin of configuration section ***
#u This is a chat program for users who can share directories,
#u for example, via a local-area network. The file with the
#u chat is stored in a shared directory.
shared_directory_pathname = r"C:\example"
#u The file with the chat should have a name that is not likely
#u to collide with any other file in that directory.
file_name = r"\chatfile_fvmchmpqtx.txt"
#u *** End of configuration section ***
progname = "Stefan Ram's LAN chat"
#p Development started approximately on 2020-09-11T19:19:58+02:00.
#p Then, there was a two-year break.
versionname = progname +" (Version 2020-09-14/--24)"
versionname = progname +" (Version 2020-09-14/2022-10-16T18:41:29+01:00)"
def follow( filepath, stream_pos, encoding="utf_8" ):
'''
This is a yield function (generator). It returns
complete '\n'-terminated lines just added to the
end of a text file, by opening that text file and
waiting for appendings of lines to that file.
This is similar to the "tail -f" command.
filepath: Path to the text file. Should be an object
that can be passed to "open" as its first argument.
stream_pos: The position of the text file from which
the lines should be taken.
encoding: The encoding of the text file.
Result: Returns immediately with an empty string
if there are no new data, otherwise it returns the
next new line read from the file.
'''
# based on "tail_F", attributed to Raymond Hettinger
with open( filepath, encoding=encoding ) as input:
input.seek( stream_pos, io.SEEK_SET )
buffer = input.read() # last line might be incomplete
while True:
if '\n' not in buffer:
buffer += input.read()
if '\n' not in buffer: # no complete line in buffer yet
yield '' # yield '' to signal "no new data avaible"
if not os.path.isfile( filepath ): break
continue
# '\n' is in latest_data, so we have read at least one
# complete line now:
# get the lines from the buffer, the last line may be
# incomplete
lines = buffer.split( '\n' )
if buffer[ -1 ]== '\n':
# if the last line is complete, fill the buffer
# from the file.
buffer = input.read()
else:
# if the last line is incomplete, fill the buffer
# from the last incomplete line.
buffer = lines[ -1 ]
# return the complete lines from the list, one line
# per "yield".
for line in lines[ :-1 ]:
yield line + '\n'
def wait_and_follow( filepath, stream_pos, encoding="utf_8", timeout=10*366*24*60*60 ):
'''
Like "follow" (above), but if the file does not exist, will
wait until the file exists or <timeout> seconds have passed.
'''
while True:
try:
yield from follow( filepath, stream_pos, encoding )
except OSError as oserror:
if oserror.errno == 2: # No such file or directory
if timeout < 3:
raise TimeoutError
time.sleep( 3 )
timeout-=3
def filepath():
'''
Return the path of the file with chat messages
'''
#p Make sure the shared directory really does exist.
shared_directory_path = pathlib.Path( shared_directory_pathname )
if not shared_directory_path.is_dir():
tkinter.messagebox.showerror( progname, f'There is no directory "{shared_directory_pathname}"! I\'m exiting.' )
sys.exit( 99 )
filepath = shared_directory_pathname + file_name
return filepath
class application_class():
__slots__ = 'filepath', 'root', 'listbox', 'entrybox', 'stream_pos', 'exiting'
def __init__( self ):
self.filepath=filepath()
self.setup_root_window()
self.stream_pos = 0
self.exiting = False
self.insert_old_messages_from_file_into_the_GUI()
self.establish_thread_to_update_titlebar()
self.establish_thread_to_update_GUI_from_file()
self.root.deiconify() # make root window visible, must go before mainloop()
tkinter.mainloop()
def setup_root_window( self ):
def create_messagelist_frame( root ):
def add_listbox( frame ):
small_font = tkinter.font.nametofont( "TkFixedFont" )
small_font.configure( size=20, weight="bold" )
listbox = tkinter.Listbox( frame, height=1, width=1, selectmode=tkinter.EXTENDED, font=small_font )
listbox.pack( side=tkinter.LEFT, expand=tkinter.YES, fill=tkinter.BOTH )
return listbox
def add_scrollbar( frame ):
scrollbar = tkinter.Scrollbar( frame )
scrollbar.pack( side=tkinter.RIGHT, fill=tkinter.Y )
return scrollbar
def connect_listbox_with_scrollbar( listbox, scrollbar ):
scrollbar.config( command=listbox.yview ) # call listbox.yview when I move
listbox.config( yscrollcommand=scrollbar.set ) # call scrollbar.set when I move
frame = tkinter.Frame( root )
listbox = add_listbox( frame )
scrollbar = add_scrollbar( frame )
connect_listbox_with_scrollbar( listbox, scrollbar )
frame.pack( expand=tkinter.YES, fill=tkinter.BOTH )
return listbox
def create_entryframe( root, entryframe_send_event ):
def create_entrybox( frame ):
entrybox = tkinter.scrolledtext.ScrolledText( frame, height=5 )
entrybox.bind( "<Control-KeyPress-Return>", lambda event: "break" )# suppress default behavior
entrybox.bind( "<Control-KeyRelease-Return>", self.entryframe_send_event )
entrybox.pack( side=tkinter.TOP, expand=tkinter.YES, fill=tkinter.BOTH )
return entrybox
def create_sendbutton( frame ):
sendbutton = tkinter.Button( frame, text="Send", command=lambda:self.entryframe_send_event(None) )
sendbutton.pack( side=tkinter.TOP, expand=tkinter.YES, fill=tkinter.X )
return sendbutton
frame = tkinter.Frame( root )
entrybox = create_entrybox( frame )
sendbutton = create_sendbutton( frame )
frame.pack( fill=tkinter.BOTH )
entrybox.focus_set()
return entrybox
self.root = tkinter.Tk()
self.root.withdraw() # now we can manipulate it without distracting the user
self.root.geometry( "512x383" )
self.root.title( versionname )
self.establish_icon()
self.listbox = create_messagelist_frame( self.root )
self.entrybox = create_entryframe( self.root, self.entryframe_send_event )
self.root.protocol( "WM_DELETE_WINDOW", self.on_closing_situation )
return
def establish_icon( self ):
height = 16
width = 16
image = tkinter.PhotoImage( height=16, width=width )
image.blank()
for x in range( width ):
for y in range( height ):
image.put( "#ffffff",( x, y ))
for x in range( width ):
for y in range( 11, 14 ):
image.put( "#000000",( x, y ))
self.root.wm_iconphoto( 'True', image )
def insert_old_messages_from_file_into_the_GUI( self ):
self.stream_pos = 0
# on the main thread, to suppress screen updating
try:
with open( self.filepath, encoding="utf_8" ) as input:
latest_data = input.read()
self.stream_pos = input.tell()
latest_lines = latest_data.split( '\n' )
self.root.config( cursor="wait" )
for line in latest_lines[:-1]:
self.listbox.insert( tkinter.END, line )
self.listbox.yview( tkinter.END )
if re.match( r"^\d\d\d\d-\d\d-\d\dT.*", line ):
self.listbox.itemconfig( tkinter.END, bg='lightgray' )
self.root.config( cursor="" )
except OSError as oserror:
pass
self.exiting = False
def establish_thread_to_update_titlebar( self ):
# establish a background thread to update the titlebar every second
freceive_thread = threading.Thread( target=self.every_second_thread )
freceive_thread.start()
def establish_thread_to_update_GUI_from_file( self ):
receive_thread = threading.Thread( target=self.receive_thread )
receive_thread.start()
# app commands
def app_cut_text_from_entry_field( self ):
# cut the text from the entry box
msg = self.entrybox.get( "1.0", "end-1c" )
self.entrybox.delete( "1.0", tkinter.END )
return msg
def app_append_text_to_file( self, msg ):
# append to the file: 1st a time stamp, 2nd the text from the entry box
try:
with open( self.filepath, "a", encoding="utf_8" ) as output:
output.write( datetime.datetime.now( datetime.timezone.utc ).astimezone().isoformat( 'T' ) + "\n" )
output.write( msg + "\n" )
except exception as e:
print( tkinter.END, "\nentrybox_send: append failed." )
def app_interpret( self, msg ):
if msg == "exit":
self.on_closing_situation()
# events
def entryframe_send_event( self, event ):
'''
The behavior for when the user has pressed the button "send" or equivalent conditions.
'''
msg = self.app_cut_text_from_entry_field()
self.app_append_text_to_file( msg )
self.app_interpret( msg )
return "break"
# situations
def on_closing_situation( self ):
self.exiting = True
self.root.destroy()
# threads
def every_second_thread( self ):
while not self.exiting:
self.root.title( versionname + " " + datetime.datetime.now( datetime.timezone.utc ).astimezone().isoformat( 'T' ))
time.sleep( 1 )
def receive_thread( self ):
for msg in wait_and_follow( self.filepath, self.stream_pos ):
if self.exiting: return
if msg == "":
if self.exiting: return
self.root.config( cursor="" )
time.sleep( 1 )
else:
self.root.config( cursor="wait" )
try:
if self.exiting: return
self.listbox.insert( tkinter.END, msg )
self.listbox.yview( tkinter.END )
if re.match( r"^\d\d\d\d-\d\d-\d\dT.*", msg ):
self.listbox.itemconfig( tkinter.END, bg='lightgray' )
except OSError as oserror:
print( oserror )
break
application = application_class() # returns only after application has ended!
# last line
--- SoupGate-Win32 v1.05
* Origin: fsxNet Usenet Gateway (21:1/5)