pythontkinterwidgetwaveform

How to interactively control the amplitude and frequency of a signal with two scale sliders using Tkinter


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

Solution

  • 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