Tkinter: autoscroll to the bottom when user is not scrolling - python-3.x

I have made a small tkinter application, that receives data from the serial port and displays them on a ScrolledText frame.
I have manaded to make the frame autoscroll down to the end, when new data appears.
There is a problem however. If the user wants to see a particular value the autoscrolling option will make him lose it.
This is why i want to make it autoscroll, only when the user is not scrolling manually.
I based my code on this answer:
Python: Scroll a ScrolledText automatically to the end if the user is not scrolling manually
This is my code:
def readSerial():
global val1
fully_scrolled_down = scrollbar.yview()[1] == 1.0
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
val1 = ser_bytes
scrollbar.insert("end", val1)
if fully_scrolled_down:
scrollbar.see("end") #autoscroll to the end of the scrollbar
However, this is not working. This code just constantly autoscrolls down, regardless of the use is manually scrolling up.
UPDATE: This is the code from the scrolledText frame:
frame2 = tk.Frame(root, bg='#80c1ff') #remove color later
frame2.place(relx=0, rely=0.1, relheight=1, relwidth=1, anchor='nw')
# make a scrollbar
scrollbar = scrolledtext.ScrolledText(frame2)
scrollbar.place(relx=0, rely=0, relheight=0.9, relwidth=1, anchor='nw')
UPDATE 2:
Full code
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports
from tkinter import scrolledtext
import time
import serial
import threading
import continuous_threading
#to be used on our canvas
HEIGHT = 700
WIDTH = 800
#hardcoded baud rate
baudRate = 9600
ser = serial.Serial('COM16', baudRate)
val1 = 0
def readSerial():
global val1
#https://stackoverflow.com/questions/51781247/python-scroll-a-scrolledtext-automatically-to-the-end-if-the-user-is-not-scroll
fully_scrolled_down = scrollbar.yview()[1] == 1.0 #remove for ayutoscroll when not afafa
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
val1 = ser_bytes
scrollbar.insert("end", val1)
if fully_scrolled_down: #remove for ayutoscroll when not afafa
scrollbar.see("end") #autoscroll to the end of the scrollbar
t1 = continuous_threading.PeriodicThread(0.1, readSerial)
#----------------------------------------------------------------------
#--------------------------------------------------------------------------------
# --- 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()
# --- frame 2 ---
frame2 = tk.Frame(root, bg='#80c1ff') #remove color later
frame2.place(relx=0, rely=0.1, relheight=1, relwidth=1, anchor='nw')
# make a scrollbar
scrollbar = scrolledtext.ScrolledText(frame2)
scrollbar.place(relx=0, rely=0, relheight=0.9, relwidth=1, anchor='nw')
# --- frame 2 ---
#--------------------------------------------------------------------------------
t1.daemon=True
t1.start()
root.mainloop() #here we run our app

yview()[1] does not consistently return 1.0 since your text widget is constantly being updated. Instead of using scrolledtext module you can create one yourself, that way you have a control over the Scrollbar's attributes. Check the below example.
from tkinter import *
import random
def foo():
val=random.randint(1000,9999)
label.config(text=val)
text.insert(END,f"{val}\n")
if vsb.get()[1]==1.0:
text.see(END)
root.after(200,foo)
root=Tk()
label=Label(root)
label.pack()
text_frame=Frame(root)
text_frame.pack()
text=Text(text_frame)
text.pack(side='left')
vsb=Scrollbar(text_frame)
vsb.pack(side='left',fill='y')
text.config(yscrollcommand=vsb.set)
vsb.config(command=text.yview)
foo()
root.mainloop()
get method of Scrollbar return a tuple of (top,bottom) coordinates accurately and you can make use of this.

Related

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()

Program stucks when sending serial data

I am developing a small application, in roder to interface with my arduino.
However, in order for the arduino to start sending serial data, i have to trigger it with a specific string.
When i press the button that will send the string and will initiate the serial read, the program stucks.
No error is shown on the terminal.
Here is the relevant code:
#setting the serial object
def on_select(event=None):
global ser
COMPort = cb.get()
ser = serial.Serial(port = COMPort, baudrate=9600)
# read serial data
def readSerial():
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
text.insert("end", ser_bytes)
after_id=root.after(100,readSerial)
# this function is triggered, when button 'Measure all Sensors' is pressed, on frame 2
def measure_all():
global stop
stop_ = False
ser.write(str.encode('rf')) #Send string 'rf to arduino', which means Measure all Sensors
readSerial() #Start Reading data
The first two functions work, because i gave been able to read serial data, when the arduino just spits data, without requiring activation by sending it text.
It's the third function that makes the program stuck somehow.
EDIT: This is more code - the bigger picture.
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
#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()'
# --- 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)
def readSerial():
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
text.insert("end", ser_bytes)
if vsb.get()[1]==1.0: #if the scrollbar is down to the bottom, then autoscroll
text.see("end")
after_id=root.after(100,readSerial)
# this function is triggered, when button 'Measure all Sensors' is pressed, on frame 2
def measure_all():
global stop_
stop_ = False
button_stop['state']='normal' #make the 'Stop Measurement' button accessible
ser.write("rf".encode()) #Send string 'rf to arduino', which means Measure all Sensors
readSerial() #Start Reading data
# --- 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()
# --- 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')
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')
#frame 6 will be the area with the texct 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 ---
stop_=True # Stop Flag. True when no measuring is happening
root.mainloop() #here we run our app
# --- main ---

Display serial incoming data on tkinter scrollbar

I downloaded a small arduino program in order for it to produce serial data.
My goal (first iteration) is to create an application that will replace the arduino IDE serial - since i only want to read serial data.
This is how the data looks like on the arduino IDE serial interface.
This is my code so far:
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports
from tkinter import scrolledtext
#new stuff
import time
import serial
import threading
import continuous_threading
#to be used on our canvas
HEIGHT = 700
WIDTH = 800
#hardcoded baud rate
baudRate = 9600
#Serial Stuff-----------------------------------------
ser = serial.Serial('COM16', baudRate)
val1 = 0
index = []
def readSerial():
global val1
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
val1 = ser_bytes
index.append(val1)
disp = tk.Label(frame2, text=index[0])
disp.config(font=("TkDefaultFont", 8))
disp.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
t1 = continuous_threading.PeriodicThread(0.5, readSerial)
# --- 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()
# --- frame 2 ---
frame2 = tk.Frame(root, bg='#80c1ff') #remove color later
frame2.place(relx=0, rely=0.1, relheight=1, relwidth=1, anchor='nw')
# make a scrollbar
scrollbar = scrolledtext.ScrolledText(frame2)
scrollbar.place(relx=0, rely=0, relheight=1, relwidth=1, anchor='nw')
# --- frame 2 ---
t1.start()
root.mainloop() #here we run our app
I have a frame2 which is a scrollText that i want my data to appear (i also have another frame above it, that i removed it here so that i don't confuse people with unnecessary code).
Ideally i would like the data to appear like in the arduino IDE, with autoscroll - but first i have to walk before i can run.
All the serial "action" happens in the portion i have marked with comments called 'Serial stuff'.
Is my placement code correct?
disp = tk.Label(frame2, text=index[0])
disp.config(font=("TkDefaultFont", 8))
disp.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
I want each line to be placed in a new line in the textbox, like my first picture
EDIT: I followed scotty3685's advice (Thanks a lot sir!) but look at what i get now at my tkinter frame:
If you compare with the first picture, it's close but it's not really there.
The way to insert text into a ScrolledText widget is as follows
s.insert("end","some_text that I want to insert")
s is the name of the scrolledtext widget (in your case this is
called scrollbar confusingly)
The first argument to insert "end"
tells the scrolled text widget to place the new text at the end of
the current text in the textbox.
and the second argument to insert is
the text you want to insert (in your case, val1).

Flickering video for tkinter video

I am trying to make a simple play/pause application in tkinter. Basically I want to show a video and have a play/pause button underneath.
So, after some research I found this suitable post to show a video using tkinter and opencv:
to show video streaming inside frame in tkinter
When using the code, given in the accepted answer to show a video, there is no problem and I don't see any flickering. Here is the code:
# import the necessary packages
from __future__ import print_function
import tkinter as tk
from PIL import ImageTk, Image
import cv2
root = tk.Tk()
# Create a frame
app = tk.Frame(root, bg="white")
app.grid()
# Create a label in the frame
lmain = tk.Label(app)
lmain.grid()
# Capture from camera
cap = cv2.VideoCapture(r'PATH_TO_VIDEO_FILE')
# function for video streaming
frame_number = 0
def video_stream():
global frame_number
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
success, frame = cap.read()
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
imgtk = ImageTk.PhotoImage(image=img)
lmain.imgtk = imgtk
lmain.configure(image=imgtk)
lmain.after(1, video_stream)
frame_number += 1
video_stream()
root.mainloop()
Now, I slightly altered the code to be able to use the grid manager and add a play button:
# import the necessary packages
from __future__ import print_function
import tkinter as tk
from PIL import ImageTk, Image
import cv2
class PhotoBoothApp:
def __init__(self, path_to_video):
# initialize the root window
self.window = tk.Tk()
self.window.title("Video_Player")
self.videocap = cv2.VideoCapture(path_to_video)
self.frame_number = 0
# Initalize
self.videocap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_number)
success, self.frame = self.videocap.read()
cv2image = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGBA)
self.img = Image.fromarray(cv2image)
self.imgtk = ImageTk.PhotoImage(image=self.img)
# Show frames
self.picture_label = tk.Label(self.window, image=self.imgtk, relief=tk.RIDGE).grid(row=0, column=0)
self.btn_next_image=tk.Button(self.window, text="Play", width=50, bg ="green",command=self.video_stream).grid(row=1,column=0)
self.window.mainloop()
def video_stream(self):
self.videocap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_number)
sucess, frame = self.videocap.read()
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
self.imgtk = ImageTk.PhotoImage(image=img)
self.picture_label = tk.Label(self.window, image=self.imgtk, relief=tk.RIDGE).grid(row=0, column=0)
# Update Frame Number to display
self.frame_number = self.frame_number + 1
self.window.after(1, self.video_stream)
ph = PhotoBoothApp(r'PATH_TO_FILE')
The problem is that when I execute the above code, the video flickers as if tkinter need to reload something in-between frames. I have no clue why this happens.
P.S. This post here Flickering video in opencv-tkinter integration did not help me.
You need to make two changes: split your self.picture_label line to create a proper reference to your Label object, and then use self.picure_label.config(...) to change the image.
class PhotoBoothApp:
def __init__(self, path_to_video):
# initialize the root window
...
self.picture_label = tk.Label(self.window, image=self.imgtk, relief=tk.RIDGE)
self.picture_label.grid(row=0, column=0)
...
def video_stream(self):
...
img = Image.fromarray(cv2image)
self.imgtk = ImageTk.PhotoImage(image=img)
self.picture_label.config(image=self.imgtk)
# Update Frame Number to display
self.frame_number = self.frame_number + 1
self.window.after(1, self.video_stream)
ph = PhotoBoothApp(r'PATH_TO_FILE')

expand scrollable ttk.notebook in python3 / gui tkinter

coding a scrollable frame with a ttk.notebook inside works.
But the scrollbar / notebook has a fixed size. How can I change it?
from tkinter import Canvas, Scrollbar, Button, Tk
from tkinter.ttk import Frame, Notebook
class VerticalScrolledFrame(Frame):
"""A pure Tkinter scrollable frame that actually works!
* Use the 'interior' attribute to place widgets inside the scrollable frame
* Construct and pack/place/grid normally
* This frame only allows vertical scrolling
"""
def __init__(self, parent, *args, **kw):
Frame.__init__(self, parent, *args, **kw)
# create a canvas object and a vertical scrollbar for scrolling it
vscrollbar = Scrollbar(self, orient='vertical')
vscrollbar.pack(fill='y', side='right', expand='false')
canvas = Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set)
canvas.pack(side='left', fill='both', expand='true')
vscrollbar.config(command=canvas.yview)
# reset the view
canvas.xview_moveto(0)
canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior,
anchor='nw')
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
canvas.config(scrollregion="0 0 %s %s" % size)
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the canvas's width to fit the inner frame
canvas.config(width=interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
def _configure_canvas(event):
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the inner frame's width to fill the canvas
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
canvas.bind('<Configure>', _configure_canvas)
root = Tk()
class Overview:
def __init__(self):
#mainframe to make a scrollable window
self.mainframe = VerticalScrolledFrame(root)
self.mainframe.grid(row=0, column=0)
# create a notebook
self.TNotebook_Overview = Notebook(self.mainframe.interior)
self.TNotebook_Overview.grid(row=0, column=0)
self.TNotebook_Overview.configure(takefocus="")
self.Frame_Overview = Frame(self.TNotebook_Overview)
self.TNotebook_Overview.add(self.Frame_Overview)
self.TNotebook_Overview.tab(0, text="Table", compound="left",underline="-1", )
buttons = []
for i in range(30):
buttons.append(Button(self.Frame_Overview, text="Button " + str(i)))
buttons[-1].grid(column=0, row=i)
if __name__ == "__main__":
ov = Overview()
root.title('Overview Items Database')
root.geometry('800x800+10+10')
root.configure(background="#4C7274")
root.grab_set()
root.mainloop()
I expect an expanded scrollable notebook/frame filled entire Tk() window.
Because the "ai" of stackoverflow don't allow use code from another thread, here is code https://pastebin.com/ykJGViAz
Inside init, do this instead
self.mainframe.grid(row=0, column=0, sticky="nsew")
self.TNotebook_Overview.grid(row=0, column=0, sticky="nsew")
This tells the frame to fill as much space as it needs
Just add this in the third line of init in the class overview:
self.mainframe.pack(fill = BOTH, expand = 1)

Resources