tkinter Text Widget duplicating when progress bar displayed - text

I have an app that displays progress messages via a Text widget, so that it will scroll if there are many lines. It also displays a 'Task' Progress Bar for each individual step, and a different 'Total' Progress Bar for the total completion.
My situation is that I want to have the progress bars not displayed if they have no 'content'. So neither of them at the start and end of the processing. The 'Total' progress bar would display once the first step is complete, and the 'Task' progress bar would be displayed if the task is long enough to warrant it.
It's a big app but here is a shortened version of what I have (still long though).
#!/usr/bin/python
# coding=utf-8
# Try to work with older version of Python
from __future__ import print_function
import sys
if sys.version_info.major < 3:
import Tkinter as tk
import Tkinter.ttk as ttk
else:
import tkinter as tk
import tkinter.ttk as ttk
#============================================================================
# MAIN CLASS
class Main(tk.Frame):
""" Main processing
"""
def __init__(self, root, *args, **kwargs):
tk.Frame.__init__(self, root, *args, **kwargs)
self.root = root
self.MW_f = tk.Frame(self.root)
# Progress Messages
self.PM_prog_msgs_lf_var = tk.StringVar()
self.PM_prog_msgs_lf = tk.LabelFrame(self.MW_f,
text=' Progress Messages: ',
relief='sunken')
# Progress Message text widget
self.PM_prog_msgs_max_frame_height = 6
self.PM_prog_msgs_line_count = 0
self.PM_prog_msgs_t = tk.Text(self.PM_prog_msgs_lf, height=self.PM_prog_msgs_max_frame_height, width=100)
self.PM_prog_msgs_t_vbar = ttk.Scrollbar(self.PM_prog_msgs_lf,
orient='vertical', command=self.PM_prog_msgs_t.yview)
self.PM_prog_msgs_t['yscrollcommand'] = self.PM_prog_msgs_t_vbar.set
self.PM_prog_msgs_t['state'] = 'disabled'
self.PM_prog_msgs_t['border'] = 3
self.PM_task_progbar_var = tk.IntVar()
self.PM_task_progbar = ttk.Progressbar(self.MW_f,
orient='horizontal',
mode='indeterminate',
variable=self.PM_task_progbar_var)
self.PM_progbar_var = tk.IntVar()
self.PM_progbar = ttk.Progressbar(self.MW_f,
orient='horizontal',
mode='determinate',
variable=self.PM_progbar_var,
maximum=4)
self.PM_go_btn = tk.Button(self.MW_f,
text='Go',
command=self.action_go_btn)
self.MW_stsbar_tvar = tk.StringVar()
self.MW_stsbar = tk.Label(self.MW_f,
textvariable=self.MW_stsbar_tvar)
# Grid the widgets
self.MW_f.grid()
MW_grid_row = 0
# Place into MW_f
self.PM_prog_msgs_lf.grid(row=MW_grid_row, column=0, sticky='we')
# Place into PM_prog_msgs_lf
self.PM_prog_msgs_t.grid(row=0, column=0)
self.PM_prog_msgs_t_vbar.grid(row=0, column=1, sticky='ns')
MW_grid_row += 1
self.PM_task_progbar_row = MW_grid_row
self.PM_task_progbar.grid(row=self.PM_task_progbar_row, sticky='we')
MW_grid_row += 1
self.PM_progbar_row = MW_grid_row
self.PM_progbar.grid(row=self.PM_progbar_row, sticky='we')
MW_grid_row += 1
self.MW_stsbar.grid(row=MW_grid_row, sticky='w')
MW_grid_row += 1
self.PM_go_btn.grid(row=MW_grid_row)
# Remove these until needed
self.PM_task_progbar.grid_remove()
self.PM_progbar.grid_remove()
self.PM_prog_msgs_t_vbar.grid_remove()
self.MW_dsp_stsbar_msg('Processing: Refer to progress message pane for more details')
# Window Displays Now
#=========================================================================================
# Functions used by window
def action_go_btn(self):
"""
"""
msg = 'Line 1\nLine 2'
self.PM_dsp_prog_msgs(msg, 2)
self.PM_task_progbar_update()
time.sleep(5)
self.PM_progbar_update()
self.PM_dsp_prog_msgs('Line 3', 1)
self.PM_task_progbar_update()
time.sleep(5)
self.PM_progbar_update()
msg = 'Line 4\nLine 5\nLine 6'
self.PM_dsp_prog_msgs(msg, 3)
self.PM_task_progbar_update()
time.sleep(5)
self.PM_progbar_update()
msg = 'Line 7\nLine 8\nLine 9'
self.PM_dsp_prog_msgs(msg, 3)
self.PM_task_progbar_update()
time.sleep(5)
self.PM_progbar_update()
self.PM_progbar.grid_remove()
return
def MW_dsp_stsbar_msg(self, msg):
"""
"""
# Display the statusbar
self.MW_stsbar_tvar.set(msg)
return
def PM_dsp_prog_msgs(self, msg, lines):
"""
"""
# Populate the message
self.PM_prog_msgs_t['state'] = 'normal'
self.PM_prog_msgs_t.insert(tk.END, ('\n'+msg))
self.PM_prog_msgs_t['state'] = 'disabled'
self.root.update_idletasks()
self.PM_prog_msgs_line_count += lines
# If its time display the vert scroll bar
if not self.PM_prog_msgs_t_vbar.winfo_viewable():
if self.PM_prog_msgs_line_count > self.PM_prog_msgs_max_frame_height:
self.PM_prog_msgs_t_vbar.grid(row=0, column=1)
self.root.update_idletasks()
# Show the last line.
self.PM_prog_msgs_t.see(tk.END)
self.root.update_idletasks()
return
def PM_progbar_update(self):
"""
"""
if not self.PM_progbar.winfo_viewable():
self.PM_progbar.grid()
# Remove the task progbar
self.PM_task_progbar.stop()
self.PM_task_progbar.grid_remove()
# Increment the progbar
self.PM_progbar.step()
self.root.update_idletasks()
return
def PM_task_progbar_update(self):
"""
"""
# Display if not already displayed
if not self.PM_task_progbar.winfo_viewable():
self.PM_task_progbar.grid()
self.root.update_idletasks()
# Step
self.PM_task_progbar.start()
self.root.update_idletasks()
return
# MAIN (MAIN) ===========================================================================
def main():
""" Run the app
"""
# # Create the screen instance and name it
root = tk.Tk()
app = Main(root)
root.mainloop()
root.quit()
# MAIN (STARTUP) ====================================================
# This next line runs the app as a standalone app
if __name__ == '__main__':
# Run the function name main()
main()
What happens is when the 'Task' progress bar is displayed the 'Progress Messages' label frame is duplicated, and moved down one line (see the screen shot)
Sorry, I cannot attach the screen shot. This is a text version of the screenshot!
Progress Messages ---------------------------------------
Progress Messages ---------------------------------------
Message Step 1
[Task Progress Bar ]
[Total Progress Bar ]
The display remains this way until the end. Even when the 'Task' progress bar is removed the duplication remains. The display does not add a third version when the 'Total' progress bar is displayed though.
Everything corrects itself at the end when both progress bars are removed.
All the widgets are gridded to the correct frame and the row/column seems to be correct, so where do I look to correct this.
Many thanks, and sorry this is all so long.
MacBookPro (2007), python 3.4, tkinter 8.5

Related

Indeterminate Progress Bar not moving

I can not get the progress bar to move. Solutions offered in other post were very convoluted. The original task was loading a workbook with openpyxl, but I have substituted a 3 second delay which presents the same problem. update() does not move the progress bar. update_idletask() does not render the window correctly. mainloop() works, but does not stop working.
I am running python 3.7.9 on windows 10.
from tkinter import *
from tkinter import ttk
import time
# from openpyxl import load_workbook
class ProgressBarIn:
def __init__(self, title="", label="", text=""):
self.title = title
self.label = label
self.text = text
self.pb_root = Tk() # create a window for the progress bar
self.pb_label = Label(self.pb_root, text=self.label) # make label for progress bar
self.pb = ttk.Progressbar(self.pb_root, length=400, mode="indeterminate") # create progress bar
self.pb_text = Label(self.pb_root, text=self.text, anchor="w")
def set_up(self):
# titlebar_icon(self.pb_root) # place icon in titlebar
self.pb_root.title(self.title)
self.pb_label.grid(row=0, column=0, sticky="w")
self.pb.grid(row=1, column=0, sticky="w")
self.pb_text.grid(row=2, column=0, sticky="w")
self.pb.start()
self.pb_root.update()
# self.pb_root.update_idletasks()
# self.pb_root.mainloop()
def stop(self):
self.pb.stop() # stop and destroy the progress bar
self.pb_label.destroy() # destroy the label for the progress bar
self.pb.destroy()
self.pb_root.destroy()
def my_function():
pb = ProgressBarIn(title="hey now", label="hold on", text="hello there")
pb.set_up()
time.sleep(3)
wb = "hello in three seconds"
# wb = load_workbook(file_path)
pb.stop()
return wb
print(my_function())
This solution uses a global as a flag which communicates to the progress bar when to stop updating after the multithread has finished it's task. The WaitUp class creates an attribute called "self.i_waited_for_this" which holds the result of the multithread's task and will be called to retrieve it once the multithread has finished. The progress bar uses time.sleep() to pause between incrementing the value of the progress bar while the global flag variable is True and stops once line 19 is read and the variable is changed to False. Just to be clear, the progress bar is running on the main thread, not an additional thread since tkinter and multithreading don't mix.
from tkinter import *
from tkinter import ttk
from threading import *
import time
class WaitUp(Thread): # Define a new subclass of the Thread class of the Thread Module.
def __init__(self, secs=0):
Thread.__init__(self) # Override the __init__
self.secs = secs
self.i_waited_for_this = ""
def run(self):
global pb_flag # this tells the progress bar to stop
for i in range(self.secs): # print one number each second / replace with your task
print(i)
time.sleep(1)
pb_flag = False
self.i_waited_for_this = "it is done" # result of the task / replace with object or variable you want to pass
class ProgressBarIn:
def __init__(self, title="", label="", text=""):
self.title = title # Build the progress bar
self.label = label
self.text = text
self.pb_root = Tk() # create a window for the progress bar
self.pb_label = Label(self.pb_root, text=self.label) # make label for progress bar
self.pb = ttk.Progressbar(self.pb_root, length=400, mode="indeterminate") # create progress bar
self.pb_text = Label(self.pb_root, text=self.text, anchor="w")
def startup(self):
self.pb_root.title(self.title)
self.pb_label.grid(row=0, column=0, sticky="w")
self.pb.grid(row=1, column=0, sticky="w")
self.pb_text.grid(row=2, column=0, sticky="w")
while pb_flag: # move the progress bar until multithread reaches line 19
self.pb_root.update()
self.pb['value'] += 1
time.sleep(.1)
def stop(self):
self.pb.stop() # stop and destroy the progress bar
self.pb_label.destroy() # destroy the label for the progress bar
self.pb.destroy()
self.pb_root.destroy()
def my_function():
global pb_flag
pb_flag = True
t1 = ProgressBarIn(title="hey now", label="hold on", text="hello there")
t2 = WaitUp(secs=5) # pass the progress bar object
t2.start() # use start() instead of run() for threading module
t1.startup() # start the progress bar
t2.join() # wait for WaitUp to finish before proceeding
t1.stop() # destroy the progress bar object
return t2.i_waited_for_this
# delclare global variables
global pb_flag # this tells the progress bar to stop
print(my_function())

Unable to read serial data without errors/bugs

I am writing a small application based on tkinter in order to read serial data from my arduino.
The arduino, when it receives a serial text (rf), it will begin sending data to the pc.
Below is the suspicious code:
def readSerial():
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
text.insert("end", ser_bytes)
after_id=root.after(100,readSerial)
#root.after(100,readSerial)
def measure_all():
global stop_
stop_ = False
ser.write("rf".encode()) #Send string 'rf to arduino', which means Measure all Sensors
readSerial() #Start Reading data
Now this does not work. The program freezes, and no info is revealed on the terminal.
When i change the line after_id=root.after(100,readSerial) to root.after(100,readSerial) then the program works, but only when i receive serial input.
For example, if there is a 5 second delay to when arduino sends serial, then the program will freeze, until it receives the data. More specificallly, if the program is minimized, and i select to view it as normal, it will not respond unless it receives input from arduino (which will display normally).
So even now, it still does not work properly.
But also have in mind, that i need to have the after_id line, so that i can have a handle, so that i can terminate the readSerial() function (for example when the user presses the 'stop measurement' button).
Can someone understand what is going on, and how i can have the after_id behaviour (so i can stop the continuous function later), while having the program behaving normal, without crashing or stuck until it receives data?
EDIT: This is the modified code after user's acw1668 suggestions. This does not work. I see nothing on the text frame of tkinter.
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports #for a list of all the COM ports
from tkinter import scrolledtext
import threading
import time
from queue import SimpleQueue
#to be used on our canvas
HEIGHT = 800
WIDTH = 800
#hardcoded baud rate
baudRate = 9600
# this is the global variable that will hold the serial object value
ser = None #initial value. will change at 'on_select()'
after_id = None
#this is the global variable that will hold the value from the dropdown for the sensor select
dropdown_value = None
# create the queue for holding serial data
queue = SimpleQueue()
# flag use to start/stop thread tasks
stop_flag = None
# --- functions ---
#the following two functtions are for the seria port selection, on frame 1
#this function populates the combobox on frame1, with all the serial ports of the system
def serial_ports():
return serial.tools.list_ports.comports()
#when the user selects one serial port from the combobox, this function will execute
def on_select(event=None):
global ser
COMPort = cb.get()
string_separator = "-"
COMPort = COMPort.split(string_separator, 1)[0] #remove everything after '-' character
COMPort = COMPort[:-1] #remove last character of the string (which is a space)
ser = serial.Serial(port = COMPort, baudrate=9600, timeout=0.1)
#readSerial() #start reading shit. DELETE. later to be placed in a button
# get selection from event
#print("event.widget:", event.widget.get())
# or get selection directly from combobox
#print("comboboxes: ", cb.get())
#ser = Serial(serialPort , baudRate, timeout=0, writeTimeout=0) #ensure non-blocking
def readSerial(queue):
global stop_flag
if stop_flag:
print("Reading task is already running")
else:
print("started")
stop_flag = threading.Event()
while not stop_flag.is_set():
if ser.in_waiting:
try:
ser_bytes = ser.readline()
data = ser_bytes.decode("utf-8")
queue.put(data)
except UnicodeExceptionError:
print("Unicode Error")
else:
time.sleep(0.1)
print("stopped")
stop_flag = None
# function to monitor whether data is in the queue
# if there is data, get it and insert into the text box
def data_monitor(queue):
if not queue.empty():
text.insert("end", f"{queue.get()}\n")
if vsb.get()[1]==1.0: #if the scrollbar is down to the bottom, then autoscroll
text.see("end")
root.after(100, data_monitor, queue)
# this function is triggered, when a value is selected from the dropdown
def dropdown_selection(*args):
global dropdown_value
dropdown_value = clicked.get()
button_single['state'] = 'normal' #when a selection from the dropdown happens, change the state of the 'Measure This Sensor' button to normal
# this function is triggered, when button 'Measure all Sensors' is pressed, on frame 2
def measure_all():
button_stop['state']='normal' #make the 'Stop Measurement' button accessible
ser.write("rf".encode()) #Send string 'rf to arduino', which means Measure all Sensors
sleep(0.05) # 50 milliseconds
threading.Thread(target=readSerial, args=(queue,)).start()
# this function is triggered, when button 'Measure this Sensor' is pressed, on frame 2
def measure_single():
global stop_
stop_=False
button_stop['state']='normal'
ser.write(dropdown_value.encode()) #Send string 'rf to arduino', which means Measure all Sensors!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
readSerial()
# this function is triggered, when button 'STOP measurement(s)' is pressed, on frame 2
def stop_measurement():
button_stop['state']='disabled'
ser.write("c".encode())
if stop_flag:
stop_flag.set()
else:
print("Reading task is not running")
# --- functions ---
# --- main ---
root = tk.Tk() #here we create our tkinter window
root.title("Sensor Interface")
#we use canvas as a placeholder, to get our initial screen size (we have defined HEIGHT and WIDTH)
canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
canvas.pack()
#we use frames to organize all the widgets in the screen
'''
relheight, relwidth − Height and width as a float between 0.0 and 1.0, as a fraction of the height and width of the parent widget.
relx, rely − Horizontal and vertical offset as a float between 0.0 and 1.0, as a fraction of the height and width of the parent widget.
'''
# --- frame 1 ---
frame1 = tk.Frame(root)
frame1.place(relx=0, rely=0.05, relheight=0.03, relwidth=1, anchor='nw') #we use relheight and relwidth to fill whatever the parent is - in this case- root
label0 = tk.Label(frame1, text="Select the COM port that the device is plugged in: ")
label0.config(font=("TkDefaultFont", 8))
label0.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
cb = ttk.Combobox(frame1, values=serial_ports())
cb.place(relx=0.5, rely=0.5, anchor='center')
# assign function to combobox, that will run when an item is selected from the dropdown
cb.bind('<<ComboboxSelected>>', on_select)
# --- frame 1 ---
# --- frame 2 ---
frame2 = tk.Frame(root, bd=5) #REMOVED THIS bg='#80c1ff' (i used it to see the borders of the frame)
frame2.place(relx=0, rely=0.1, relheight=0.07, relwidth=1, anchor='nw')
#Button for 'Measure All Sensors'
#it will be enabled initially
button_all = tk.Button(frame2, text="Measure all Sensors", bg='#80c1ff', fg='red', state='normal', command=measure_all) #bg='gray'
button_all.place(relx=0.2, rely=0.5, anchor='center')
#label
label1 = tk.Label(frame2, text="OR, select a single sensor to measure: ")
label1.config(font=("TkDefaultFont", 9))
label1.place(relx = 0.32, rely=0.3, relwidth=0.3, relheight=0.4)
#dropdown
#OPTIONS = [0,1,2,3,4,5,6,7]
OPTIONS = list(range(8)) #[0,1,2,3,4,5,6,7]
clicked = tk.StringVar(master=frame2) # Always pass the `master` keyword argument, in order to run the function when we select from the dropdown
clicked.set(OPTIONS[0]) # default value
clicked.trace("w", dropdown_selection) #When a value from the dropdown is selected, function dropdown_selection() is executed
drop = tk.OptionMenu(frame2, clicked, *OPTIONS)
drop.place(relx = 0.65, rely=0.25, relwidth=0.08, relheight=0.6)
#Button for 'Measure Single Sensor'
#this will be disabled initially, and will be enabled when an item from the dropdown is selected
button_single = tk.Button(frame2, text="Measure this Sensor", bg='#80c1ff', fg='red', state='disabled', command=measure_single) #bg='gray'
button_single.place(relx = 0.85, rely=0.5, anchor='center')
# --- frame 2 ---
# --- frame 3 ---
frame3 = tk.Frame(root, bd=5) #REMOVED THIS bg='#80c1ff' (i used it to see the borders of the frame)
frame3.place(relx=0, rely=0.2, relheight=0.07, relwidth=1, anchor='nw')
#Button for 'STOP Measurement(s)'
#this will be disabled initially, and will be enabled only when a measurement is ongoing
button_stop = tk.Button(frame3, text="STOP measurement(s)", bg='#80c1ff', fg='red', state='disabled', command=stop_measurement)
button_stop.place(relx=0.5, rely=0.5, anchor='center')
# --- frame 3 ---
# --- frame 4 ---
frame4 = tk.Frame(root, bd=5)
frame4.place(relx=0, rely=0.3, relheight=0.09, relwidth=1, anchor='nw')
label2 = tk.Label(frame4, text="Select a sensor to plot data: ")
label2.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
clickedForPlotting = tk.StringVar()
clickedForPlotting.set(OPTIONS[0]) # default value
dropPlot = tk.OptionMenu(frame4, clickedForPlotting, *OPTIONS)
dropPlot.place(relx=0.5, rely=0.5, anchor='center')
#CHANGE LATER
#dropDownButton = tk.Button(frame4, text="Plot sensor data", bg='#80c1ff', fg='red', command=single_Sensor) #bg='gray'
#dropDownButton.place(relx = 0.85, rely=0.5, anchor='center')
# --- frame 4 ---
#frame 5 will be the save to txt file
#frame 6 will be the area with the text field
# --- frame 6 ---
frame6 = tk.Frame(root, bg='#80c1ff') #remove color later
frame6.place(relx=0.0, rely=0.4, relheight=1, relwidth=1, anchor='nw')
text_frame=tk.Frame(frame6)
text_frame.place(relx=0, rely=0, relheight=0.6, relwidth=1, anchor='nw')
text=tk.Text(text_frame)
text.place(relx=0, rely=0, relheight=1, relwidth=1, anchor='nw')
vsb=tk.Scrollbar(text_frame)
vsb.pack(side='right',fill='y')
text.config(yscrollcommand=vsb.set)
vsb.config(command=text.yview)
# --- frame 6 ---
# start data monitor task
data_monitor(queue)
root.mainloop() #here we run our app
# --- main ---
In order to not blocking the main tkinter application, it is recommended to use thread to run the serial reading. Also use queue.SimpleQueue to transfer the serial data to main task so that the serial data can be inserted into the Text widget.
Below is an example:
import threading
import time
from queue import SimpleQueue
import tkinter as tk
import serial
class SerialReader(threading.Thread):
def __init__(self, ser, queue, *args, **kw):
super().__init__(*args, **kw)
self.ser = ser
self.queue = queue
self._stop_flag = threading.Event()
def run(self):
print("started")
while not self._stop_flag.is_set():
if self.ser.in_waiting:
ser_bytes = self.ser.readline()
data = ser_bytes.decode("utf-8")
self.queue.put(data)
else:
time.sleep(0.1)
print("stopped")
def terminate(self):
self._stop_flag.set()
# create the serial instance
ser = serial.Serial(port="COM1") # provide other parameters as well
# create the queue for holding serial data
queue = SimpleQueue()
# the serial reader task
reader = None
def start_reader():
global reader
if reader is None:
# create the serial reader task
reader = SerialReader(ser, queue, daemon=True)
if not reader.is_alive():
# start the serial reader task
reader.start()
else:
print("Reader is already running")
def stop_reader():
global reader
if reader and reader.is_alive():
# stop the serial reader task
reader.terminate()
reader = None
else:
print("Reader is not running")
# function to monitor whether data is in the queue
# if there is data, get it and insert into the text box
def data_monitor(queue):
if not queue.empty():
text.insert("end", f"{queue.get()}\n")
root.after(100, data_monitor, queue)
root = tk.Tk()
text = tk.Text(root, width=80, height=20)
text.pack()
frame = tk.Frame(root)
tk.Button(frame, text="Start", command=start_reader).pack(side="left")
tk.Button(frame, text="Stop", command=stop_reader).pack(side="left")
frame.pack()
# start data monitor task
data_monitor(queue)
root.mainloop()
Update#2021-04-16: Example without using class
import threading
import time
from queue import SimpleQueue
import tkinter as tk
import serial
# create the serial instance
ser = serial.Serial(port="COM1") # provide other parameters as well
# create the queue for holding serial data
queue = SimpleQueue()
# flag use to start/stop thread tasks
stop_flag = None
def readSerial(queue):
global stop_flag
if stop_flag:
print("Reading task is already running")
else:
print("started")
stop_flag = threading.Event()
while not stop_flag.is_set():
if ser.in_waiting:
ser_bytes = ser.readline()
data = ser_bytes.decode("utf-8")
queue.put(data)
else:
time.sleep(0.1)
print("stopped")
stop_flag = None
def start_reader():
threading.Thread(target=readSerial, args=(queue,)).start()
def stop_reader():
if stop_flag:
stop_flag.set()
else:
print("Reading task is not running")
# function to monitor whether data is in the queue
# if there is data, get it and insert into the text box
def data_monitor(queue):
if not queue.empty():
text.insert("end", f"{queue.get()}\n")
root.after(100, data_monitor, queue)
root = tk.Tk()
text = tk.Text(root, width=80, height=20)
text.pack()
frame = tk.Frame(root)
tk.Button(frame, text="Start", command=start_reader).pack(side="left")
tk.Button(frame, text="Stop", command=stop_reader).pack(side="left")
frame.pack()
# start data monitor task
data_monitor(queue)
root.mainloop()

TkInter - Can't Get Frames to work correctly and resize

TkInter's frames are driving me crazy. My goal is to have an options frame where I can select some options, then press "Archive" and the TkInter window changes to showing the output from the rest of my script.
I cannot get this to size correctly - there appears to be some additional frame taking up space in the window.
import string
from tkinter import *
import tkinter as tk
import threading
def main(argv):
print("In Main")
for arg in argv:
print(arg)
class TextOut(tk.Text):
def write(self, s):
self.insert(tk.CURRENT, s)
self.see(tk.END)
def flush(self):
pass
class Mainframe(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self._frame = OptionsFrame(self)
self._frame.pack(expand=True)
def change(self, frameClass):
# make new frame - for archive output
self._frame = frameClass(self)
self._frame.pack(fill="both", expand=True)
return self._frame
class Mainframe(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self._frame = OptionsFrame(self)
self._frame.pack(expand=True)
def change(self, newFrameClass):
# make new frame - for archive output
self._frame = newFrameClass(self)
self._frame.pack(fill="both", expand=True)
return self._frame
class OptionsFrame(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
master.title("Test")
master.geometry("325x180")
self.selectedProject = None
self.initUI(master)
def initUI(self, master):
frame1 = Frame(master)
frame1.pack(fill=BOTH, expand=True)
self.label1 = Label(frame1, text="Select Project to Archive, then click Archive")
self.projectListbox = tk.Listbox(frame1, width=60, height=100)
self.projectListbox.bind("<<ProjectSelected>>", self.changeProject)
# create a vertical scrollbar for the listbox to the right of the listbox
self.yscroll = tk.Scrollbar(self.projectListbox,command=self.projectListbox.yview,orient=tk.VERTICAL)
self.projectListbox.configure(yscrollcommand=self.yscroll.set)
# Archive button
self.archiveBtn=tk.Button(frame1,text="Archive",command=self.ArchiveButtonClick)
# Do layout
self.label1.pack()
self.projectListbox.pack(fill="both", expand=True)
self.yscroll.pack(side="right", fill="y")
self.archiveBtn.pack(side="bottom", pady=10, expand=False)
choices = ["test 1", "test 2", "test 3", "test 4", "test 5", "test 6"]
# load listbox with sorted data
for item in choices:
self.projectListbox.insert(tk.END, item)
def getSelectedProject(self):
# get selected line index
index = self.projectListbox.curselection()[0]
# get the line's text
return self.projectListbox.get(index)
# on change dropdown value
def changeProject(self,*args):
self.selectedProject = self.getSelectedProject()
def ArchiveButtonClick(self):
# Switch to second frame - for running the archive
self.changeProject(None)
# Hide existing controls
self.label1.pack_forget()
self.projectListbox.pack_forget()
self.yscroll.pack_forget()
self.archiveBtn.pack_forget()
newFrame = self.master.change(ArchivingOutputFrame)
newFrame.args = [ "-n", self.selectedProject]
newFrame.start()
# Frame shown while archive task is running
class ArchivingOutputFrame(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
master.title("Test Frame 2")
master.geometry("1000x600")
# Set up for standard output in window
self.var = tk.StringVar(self)
lbl = tk.Label(self, textvariable=self.var)
#lbl.grid(row=0, column=0)
lbl.pack(anchor="nw")
def start(self):
t = threading.Thread(target=self.process)
t.start()
def process(self):
main(self.args)
if __name__=="__main__":
# If command line options passed - skip the UI
if len(sys.argv) > 1:
main(sys.argv[1:])
else:
app=Mainframe()
text = TextOut(app)
sys.stdout = text
sys.stderr = text
text.pack(expand=True, fill=tk.BOTH)
app.mainloop()
Here is what I get in the UI; note this is showing the UI hierachy from Microsoft's Spy++ - there is a frame I didn't create (at least I don't think I did) that is at the bottom of the window and taking up half of the UI area; this is the yellow highlight. My options pane is thus squeezed into the top half.
Resize also doesn't work - if I resize the window, I get this:
When I click the button and the code to remove the options frame and put in the frame that is capturing stdout/stderr from the main script runs, I get this:
Now the extra space appears to be at the top!
Thanks for any ideas - I know I could switch to using the "Grid" UI layout engine, but this seems so simple - I'm not doing anything sophisticated here that shouldn't work with pack.
That was a lot of complicated code. It would be easier to debug if you provide a Minimal, Complete, and Verifiable example.
However; the bottom Frame is the TextOut() widget that you pack after Mainframe():
if __name__=="__main__":
app = Mainframe()
text = TextOut(app) # This one
sys.stdout = text
sys.stderr = text
text.pack(expand=True, fill=tk.BOTH)
app.mainloop()
You'll have an easier time debugging if you give each widget a bg colour and then give them all some padding so you can easier identify which widget is inside which widget.

How to make window overlay (on top of browser,games exc.) with wxPython

I want to make a simple program (this code is a demo), that will collect system data and display it on top of everything. My goal is to create an overall ping collector for the current biggest internet user.
All I'm asking for is how to make a overlay nothing more.
"""
Hello World, but with more meat.
"""
import wx
class HelloFrame(wx.Frame):
"""
A Frame that says Hello World
"""
def __init__(self, *args, **kw):
# ensure the parent's __init__ is called
super(HelloFrame, self).__init__(*args, **kw)
# create a panel in the frame
pnl = wx.Panel(self)
# and put some text with a larger bold font on it
st = wx.StaticText(pnl, label="Hello World!", pos=(25,25))
font = st.GetFont()
font.PointSize += 10
font = font.Bold()
st.SetFont(font)
# create a menu bar
self.makeMenuBar()
# and a status bar
self.CreateStatusBar()
self.SetStatusText("Welcome to wxPython!")
def makeMenuBar(self):
"""
A menu bar is composed of menus, which are composed of menu items.
This method builds a set of menus and binds handlers to be called
when the menu item is selected.
"""
# Make a file menu with Hello and Exit items
fileMenu = wx.Menu()
# The "\t..." syntax defines an accelerator key that also triggers
# the same event
helloItem = fileMenu.Append(-1, "&Hello...\tCtrl-H",
"Help string shown in status bar for this menu item")
fileMenu.AppendSeparator()
# When using a stock ID we don't need to specify the menu item's
# label
exitItem = fileMenu.Append(wx.ID_EXIT)
# Now a help menu for the about item
helpMenu = wx.Menu()
aboutItem = helpMenu.Append(wx.ID_ABOUT)
# Make the menu bar and add the two menus to it. The '&' defines
# that the next letter is the "mnemonic" for the menu item. On the
# platforms that support it those letters are underlined and can be
# triggered from the keyboard.
menuBar = wx.MenuBar()
menuBar.Append(fileMenu, "&File")
menuBar.Append(helpMenu, "&Help")
# Give the menu bar to the frame
self.SetMenuBar(menuBar)
# Finally, associate a handler function with the EVT_MENU event for
# each of the menu items. That means that when that menu item is
# activated then the associated handler function will be called.
self.Bind(wx.EVT_MENU, self.OnHello, helloItem)
self.Bind(wx.EVT_MENU, self.OnExit, exitItem)
self.Bind(wx.EVT_MENU, self.OnAbout, aboutItem)
def OnExit(self, event):
"""Close the frame, terminating the application."""
self.Close(True)
def OnHello(self, event):
"""Say hello to the user."""
wx.MessageBox("Hello again from wxPython")
def OnAbout(self, event):
"""Display an About Dialog"""
wx.MessageBox("This is a wxPython Hello World sample",
"About Hello World 2",
wx.OK|wx.ICON_INFORMATION)
if __name__ == '__main__':
# When this module is run (not imported) then create the app, the
# frame, show it, and start the event loop.
app = wx.App()
frm = HelloFrame(None, title='Hello World 2')
frm.Show()
app.MainLoop()
And this is my code so far for the ping collecting "it is still in progress of making and obviously I'll have to modify it a bit + a lot of optimizing.
The topic is on overlay not on this code.
import os
x= os.system('netstat -on > log.txt')
dat = open('log.txt','r')
line = dat.readlines()
dat.close()
list = []
line = line[4:] #removes irrelevant stuff
for x in line:
y = ' '.join(x.split())
if y != '':
list.append(y) #y[1:] tcp irrelevant but i'll keep it
for x in range(len(list)):
list[x] = list[x].split(' ')
top = 0
for x in range(len(list)):
count = 0
for y in range(len(list)):
if list[x][4] == list[y][4]:
count= count+1
if count > top:
top = count
ip = list[x]
ip = ''.join(ip[2].partition(':')[:1])
os.system('ping '+ip+' -n 3 > log.txt') # -n 3 repeat ping 3 times
dat = open('log.txt','r')
ping = dat.readlines()
dat.close()
ping = ping[len(ping)-1:]
print('Ping for ip: '+ip+' '+' '.join(ping[0].split()))
os.system('del log.txt') #useless stuff
input('')
This link is a good tutorial for frame styling/transparent overlays. Here is my favorite code from the tutorial, in which you make a transparent, movable, gray overlay:
import wx
class FancyFrame(wx.Frame):
def __init__(self):
style = ( wx.CLIP_CHILDREN | wx.STAY_ON_TOP | wx.FRAME_NO_TASKBAR |
wx.NO_BORDER | wx.FRAME_SHAPED )
wx.Frame.__init__(self, None, title='Fancy', style = style)
self.Bind(wx.EVT_KEY_UP, self.OnKeyDown)
self.Bind(wx.EVT_MOTION, self.OnMouse)
self.SetTransparent( 220 )
self.Show(True)
def OnKeyDown(self, event):
"""quit if user press q or Esc"""
if event.GetKeyCode() == 27 or event.GetKeyCode() == ord('Q'): #27 is Esc
self.Close(force=True)
else:
event.Skip()
def OnMouse(self, event):
"""implement dragging"""
if not event.Dragging():
self._dragPos = None
return
self.CaptureMouse()
if not self._dragPos:
self._dragPos = event.GetPosition()
else:
pos = event.GetPosition()
displacement = self._dragPos - pos
self.SetPosition( self.GetPosition() - displacement )
app = wx.App()
f = FancyFrame()
app.MainLoop()
Here is the output of the code:
PS: The code window won't show in the TaskBar

How to use multithreading with tkinter to update a label and simultaneously perform calculations in a thread controlled by button events?

I'm trying to start a counter that displays the value in a label on a separate display window.
The main window has a START, STOP and DISPLAY button.
The START must start the counter, STOP must stop it and the display window must open only when I click on DISPLAY button.
Here's what I have so far. The buttons seem to be unresponsive and the display window pops up without user intervention. How can I fix this?
import tkinter as tk
import time
import threading
import queue
def Run_Device(disp_q,flaq_q):
temp_q = queue.Queue()
temp_q.put(0)
while(flaq_q.empty()):
#time.sleep(0.2)
count = temp_q.get()
count += 1
temp_q.put(count)
disp_q.put(count)
else:
flaq_q.queue.clear()
def P_Window(disp_q):
pw = tk.Tk()
value_label = tk.Label(pw, text=disp_q.get(), relief='sunken', bg='lemon chiffon', font='Helvetica 16 bold')
value_label.pack()
def update_values():
value_label.config(text=disp_q.get())
value_label.after(1000,update_values)
update_values()
pw.mainloop()
def Stop_Dev(flaq_q):
flaq_q.put("Stop")
if __name__ == "__main__":
disp_q = queue.Queue()
flaq_q = queue.Queue()
t_device = threading.Thread(target=Run_Device, args=(disp_q, flaq_q), name="Device 1")
t_disp = threading.Thread(target=P_Window, args=(disp_q, ), name="Display 1")
window = tk.Tk()
start_button = tk.Button(window, text='Start', command=t_device.start(), bg='spring green', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
start_button.pack()
stop_button = tk.Button(window, text='Stop', command=lambda: Stop_Dev(flaq_q), bg='OrangeRed2', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
stop_button.pack()
disp_param_button = tk.Button(window, text='Display', command=t_disp.start(), bg='sky blue', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
disp_param_button.pack()
window.mainloop()
I'm trying to learn how to use multithreading in tkinter so any feedback would be appreciated
There are two issues I see with your code. The first is just simple bugs, e.g.:
start_button = tk.Button(window, text='Start', command=t_device.start(), ...
here it should be command=t_device.start otherwise the command is the function returned by t_device.start()
The second is you've not dealt with various "What if ...?" scenarios, e.g. What if the user pushes 'Start' or 'Display' multiple times?
I've tried to address the above in my rework below:
import tkinter as tk
from time import sleep
from queue import Queue, Empty
from threading import Thread
FONT = 'Helvetica 16 bold'
def run_device():
count = 0
while flaq_q.empty():
count += 1
disp_q.put(count)
sleep(0.5)
while not flaq_q.empty(): # flaq_q.queue.clear() not documented
flaq_q.get(False)
def p_window():
global pw
if pw is None:
pw = tk.Toplevel()
value_label = tk.Label(pw, text=disp_q.get(), width=10, font=FONT)
value_label.pack()
def update_values():
if not disp_q.empty():
try:
value_label.config(text=disp_q.get(False))
except Empty:
pass
pw.after(250, update_values)
update_values()
elif pw.state() == 'normal':
pw.withdraw()
else:
pw.deiconify()
def stop_device():
if flaq_q.empty():
flaq_q.put("Stop")
def start_device():
global device
if device and device.is_alive():
return
while not disp_q.empty():
disp_q.get(False)
disp_q.put(0)
device = Thread(target=run_device)
device.start()
if __name__ == "__main__":
disp_q = Queue()
flaq_q = Queue()
root = tk.Tk()
pw = None
device = None
tk.Button(root, text='Start', command=start_device, width=20, font=FONT).pack()
tk.Button(root, text='Stop', command=stop_device, width=20, font=FONT).pack()
tk.Button(root, text='Display', command=p_window, width=20, font=FONT).pack()
root.mainloop()
I've left out some details to simplify the example. Even with its additional checks, it's still nowhere near perfect. (E.g. it hangs if you don't 'Stop' before you close the window, etc.)
You might be able to adapt something like the following:
import tkinter as tk
import threading
import queue
import time
def run_device():
for i in range(4):
print(i)
pass
def process_queue(MyQueue):
"""Check if we got a complete message from our thread.
Start the next operation if we are ready"""
try:
# when our thread completes its target function (task),
# the supplied message is added to the queue
msg = MyQueue.get(0)
# check message
print(msg)
except queue.Empty:
# .get failed, come back in 100 and check again
print('no message')
threading.Timer(0.001, lambda q=MyQueue: process_queue(q)).start()
class ThreadedTask(threading.Thread):
"""threaded task handler"""
def __init__(self, queue, target, msg):
threading.Thread.__init__(self)
# message to add to queue when the task (target function) completes
self.msg = msg
# function to run
self._target = target
# queue to store completion message
self.queue = queue
def run(self):
"""called when object is instantiated"""
# start users task
try:
self._target()
except Exception as e:
self.queue.put('Thread Fail')
return
# we are done, pass the completion message
self.queue.put(self.msg)
if __name__ == '__main__':
MyQueue = queue.Queue()
MyThread = ThreadedTask(MyQueue, run_device, 'Thread Task: Run')
MyThread.start()
process_queue(MyQueue)

Resources