Just starting out with python, I want to model a triangular signal and be able to control its amplitude and frequency in an interactive way using scale/sliders widgets, and Tkinter
as possible.
Inappropriately, my (updated) code below generate two signals (amplitude and frequency) which are controlled by the sliders independently of each other. This isn't surprising because I didn't link these two. However, there are no examples on internet explaining in a didactic way how to do it (matplotlib
, in particular .set_ydata
allows this interactivity -see here- but I would really like to understand in detail the binding process and if this is possible with Tkinter
).
So, the question is: How to bind the 2 sliders so that they control a single signal, that is to say when we move one, the other variable remains in its last position?
Thanks for help or any advice
Code:
# import modules
from tkinter import ttk
import tkinter as tk
# create window
root = tk.Tk() # object root
root.title('Oscilloscope')
root.geometry("1200x600+200+100")
# exit button
btn_exit = tk.Button(root, text='Exit', command=root.destroy, height=2, width=15)
btn_exit.place(x=1100, y=500, anchor=tk.CENTER)
# canvas
canvas = tk.Canvas(root, width = 800, height = 400, bg = 'white')
canvas.place(x=600, y=250, anchor=tk.CENTER)
for x in range(0, 800, 50): canvas.create_line(x, 0, x, 400, fill='darkgray', dash=(2, 2)) # vertical dashed lines every 50 units
for x in range(0, 400, 50): canvas.create_line(0, x, 800, x, fill='darkgray', dash=(2, 2)) # horizontal dashed lines every 50 units
canvas.create_line(400, 0, 400, 800, fill='black', width=2) # vertical line at x = 400
canvas.create_line(0, 200, 800, 200, fill='black', width=2) # horizontal line at y = 200
canvas.create_rectangle(3, 3, 800, 400, width=2, outline='darkgrey')
# parameters triangular signal
amplitude = 200
frequency = 10
nb_pts = 20 # must be necessary 2 fold the frequency for a triangular signal
offset = 200
# function for drawing the triangular signal
def draw_triangular(canvas, amplitude, frequency, offset, nb_pts):
xpts = 1000 / (nb_pts-1)
line = []
for i in range(nb_pts):
x = i * xpts
y = amplitude * ((2 * (i * frequency / nb_pts) % 2 - 1)) + offset
line.extend((x, y))
canvas_line = canvas.create_line(line, fill="red", width=3)
canvas.after(50, canvas.delete, canvas_line)
# vertical widget scale for amplitude ###############################################################################################
def amplitude_value(new_value): # show value
label_amplitude.configure(text=f"Amplitude {new_value}")
def select_amplitude():
sel = "Value = " + str(value.get(amplitude))
value_amp = tk.IntVar()
frm = ttk.Frame(root, padding=10)
frm.place(x=100, y=250, anchor=tk.CENTER)
scale_amplitude = tk.Scale(frm, variable=value_amp, command=amplitude_value,
from_ = 200, to = -200, length=400, showvalue=0, tickinterval=50, orient = tk.VERTICAL)
scale_amplitude.pack(anchor=tk.CENTER, padx=10)
label_amplitude = ttk.Label(root, text="Amplitude", font=("Arial"))
label_amplitude.place(x=110, y=480, anchor=tk.CENTER)
def update_amplitude():
amplitude = scale_amplitude.get()
draw_triangular(canvas, amplitude, frequency, offset, nb_pts)
root.after(50, update_amplitude)
return amplitude
# horizontal widget scale for frequency ###########################################################################################
def frequency_value(new_value): # show value
label_frequency.configure(text=f"Frequency {new_value}")
def select_frequency():
sel = "Value = " + str(value.get(frequency))
value_freq = tk.IntVar()
frm = ttk.Frame(root, padding=10)
frm.place(x=600, y=520, anchor=tk.CENTER)
scale_frequency = tk.Scale(frm, variable=value_freq, command=frequency_value,
from_ = -50, to = 50, length=800, showvalue=0, tickinterval=10, orient = tk.HORIZONTAL)
scale_frequency.pack(anchor=tk.CENTER, padx=10)
label_frequency = ttk.Label(root, text="Frequency", font=("Arial"))
label_frequency.place(x=600, y=560, anchor=tk.CENTER)
def update_frequency():
frequency = scale_frequency.get()
draw_triangular(canvas, amplitude , frequency, offset, nb_pts)
root.after(50, update_frequency)
return frequency
# reset function
def reset_values():
value_amp.set(0)
amplitude_value(0)
value_freq.set(0)
frequency_value(0)
# reset button
btn_reset = tk.Button(root, text='Reset', command=reset_values, height=2, width=15)
btn_reset.place(x=1100, y=400, anchor=tk.CENTER)
update_amplitude()
update_frequency()
root.mainloop()
Edit
Thanks to acw1668's answer, the two sliders now interact and the signal is fully controllable, as updated in the new version of the code below (note that now, the triangular signal is built using signal
from scipy
).
Thanks for all
Updated code
# import modules
from tkinter import ttk
import tkinter as tk
import numpy as np
from scipy import signal as sg
# create window
root = tk.Tk() # object root
root.title('Oscilloscope')
root.geometry("1200x600+200+100")
# exit button
btn_exit = tk.Button(root, text='Exit', command=root.destroy, height=2, width=15)
btn_exit.place(x=1100, y=500, anchor=tk.CENTER)
# canva of internal grid
canvas = tk.Canvas(root, width = 800, height = 400, bg = 'white')
canvas.place(x=600, y=250, anchor=tk.CENTER)
for x in range(0, 800, 50): canvas.create_line(x, 0, x, 400, fill='darkgray', dash=(2, 2))
for x in range(0, 400, 50): canvas.create_line(0, x, 800, x, fill='darkgray', dash=(2, 2))
canvas.create_line(400, 0, 400, 800, fill='black', width=2)
canvas.create_line(0, 200, 800, 200, fill='black', width=2)
canvas.create_rectangle(3, 3, 800, 400, width=2, outline='darkgrey')
# parameters triangular signal
nb_pts = 2500
x_range = 800
offset = 200
# updated draw_triangular()
def draw_triangular(canvas, amplitude, frequency, offset, nb_pts):
canvas.delete("line") # clear current plot
x_pts = x_range / (nb_pts-1)
line = []
for i in range(nb_pts):
x = (i * x_pts)
y = amplitude * sg.sawtooth(2 * np.pi * frequency * i/nb_pts, width=0.5) + offset
line.extend((x, y))
canvas.create_line(line, fill="red", width=3, tag="line")
# function to be called when any of the scales is changed
def on_scale_changed(*args):
amplitude = value_amp.get()
frequency = value_freq.get()
draw_triangular(canvas, amplitude, frequency, offset, nb_pts)
# vertical widget scale for amplitude ###############################################################################################
def select_amplitude():
sel = "Value = " + str(value.get(amplitude))
value_amp = tk.IntVar()
frm = ttk.Frame(root, padding=10)
frm.place(x=100, y=250, anchor=tk.CENTER)
scale_amplitude = tk.Scale(frm, variable=value_amp, command=on_scale_changed,
from_ = 200, to = -200, length=400, showvalue=1, tickinterval=50, orient = tk.VERTICAL)
scale_amplitude.pack(anchor=tk.CENTER, padx=10)
label_amplitude = ttk.Label(root, text="Amplitude", font=("Arial"))
label_amplitude.place(x=110, y=480, anchor=tk.CENTER)
# horizontal widget scale for frequency #############################################################################################
def select_frequency():
sel = "Value = " + str(value.get(frequency))
value_freq = tk.IntVar()
frm = ttk.Frame(root, padding=10)
frm.place(x=600, y=480, anchor=tk.CENTER)
scale_frequency = tk.Scale(frm, variable=value_freq, command=on_scale_changed,
from_ = 0, to = 50, length=800, showvalue=1, tickinterval=5, orient = tk.HORIZONTAL)
scale_frequency.pack(anchor=tk.CENTER, padx=10)
label_frequency = ttk.Label(root, text="Frequency", font=("Arial"))
label_frequency.place(x=600, y=530, anchor=tk.CENTER)
# reset function
def reset_values():
value_amp.set(0)
value_freq.set(0)
canvas.delete("line")
# reset button
btn_reset = tk.Button(root, text='Reset', command=reset_values, height=2, width=15)
btn_reset.place(x=1100, y=400, anchor=tk.CENTER)
root.mainloop()
You can simply bind the command
option of the two scales to same function, and get the values of amplitude and frequency inside that function and call draw_triangular()
with these values.
...
# updated draw_triangular()
def draw_triangular(canvas, amplitude, frequency, offset, nb_pts):
canvas.delete("line") # clear current plot
xpts = 1000 / (nb_pts-1)
line = []
for i in range(nb_pts):
x = i * xpts
y = amplitude * ((2 * (i * frequency / nb_pts) % 2 - 1)) + offset
line.extend((x, y))
canvas.create_line(line, fill="red", width=3, tag="line")
...
# function to be called when any of the scales is changed
def on_scale_changed(*args):
amplitude = value_amp.get()
frequency = value_freq.get()
draw_triangular(canvas, amplitude, frequency, offset, nb_pts)
...
scale_amplitude = tk.Scale(frm, variable=value_amp, command=on_scale_changed,
from_ = 200, to = -200, length=400, showvalue=0, tickinterval=50, orient = tk.VERTICAL)
...
scale_frequency = tk.Scale(frm, variable=value_freq, command=on_scale_changed,
from_ = -50, to = 50, length=800, showvalue=0, tickinterval=10, orient = tk.HORIZONTAL)
...
# reset function
def reset_values():
value_amp.set(0)
value_freq.set(0)
canvas.delete("line")
...
Note that in this case, you don't need to call the below two after loops.
update_amplitude() # don't call it
update_frequency() # don't call it