For one of the final 100 Days of Python, I need to build a PDF to audiobook program. The requirements aren't very clear on whether it needs to be an app or just write a script, so I decided to build it in Tkinter. In addition to just reading the text, I also want the user to be able to do things like adjust the volume, rate, and gender of the voice while the text is being read. However, when I press the play button, the GUI freezes until it's finished reading, and pressing the volume/etc. buttons has no effect.
Is this something that is feasible in Tkinter? I read some about threading and how Tkinter doesn't do very well with running multiple processes at a time. I tried putting the play button in a separate thread. When I did this, I could press the volume/etc. buttons, but they did not change the audio at all. Something about how the main loop can't change other threads?
Should I abandon doing this in Tkinter and try something else, like a Flask app, or is it possible to do this and I'm just missing something?
from tkinter import *
import pyttsx3
class App(Tk):
def __init__(self):
super().__init__()
self.engine = pyttsx3.init()
self.rate = 150
self.volume = 0.5
self.voices = self.engine.getProperty("voices")
self.title("DIY Audiobooks")
self.background_color = "#A7C4BC"
self.config(padx=20, pady=20, bg=self.background_color)
self.heading_font = "Cambria"
self.body_font = "Times New Roman"
self.title_label = Label(text="DIY Audiobooks",
font=(self.heading_font, 40, "bold"),
bg=self.background_color)
self.title_label.grid(row=0, column=0, columnspan=3)
## Volume Buttons ##
self.volume_up_img = PhotoImage(file="volume-up.png")
self.volume_down_img = PhotoImage(file="volume-down.png")
self.mute_image = PhotoImage(file="silent.png")
self.volume_up_button = Button(image=self.volume_up_img,
command=self.increase_volume)
self.volume_down_button = Button(image=self.volume_down_img,
command=self.decrease_volume)
self.mute_button = Button(image=self.mute_image,
command=self.mute_volume)
self.volume_up_button.grid(row=1, column=0)
self.volume_down_button.grid(row=1, column=1)
self.mute_button.grid(row=1, column=2)
## Rate Buttons ##
self.faster_img = PhotoImage(file="fast-forward-button.png")
self.slower_img = PhotoImage(file="fast-backward.png")
self.rate_up_button = Button(image=self.faster_img,
command=self.increase_rate)
self.rate_down_button = Button(image=self.slower_img,
command=self.decrease_rate)
self.rate_up_button.grid(row=2, column=0)
self.rate_down_button.grid(row=2, column=1)
## Voice Buttons ##
self.male = PhotoImage(file="male.png")
self.female = PhotoImage(file="woman.png")
self.select_voice_label = Label(text="Choose a male or female voice.",
font=(self.body_font, 14),
bg=self.background_color)
self.select_voice_label.grid(row=3, column=0, columnspan=2)
self.male_voice = Button(image=self.male,
command=self.change_to_male)
self.female_voice = Button(image=self.female,
command=self.change_to_female)
self.male_voice.grid(row=4, column=0)
self.female_voice.grid(row=4, column=1)
## Play and Pause Buttons ##
self.play_img = PhotoImage(file="play-button.png")
self.play_button = Button(image=self.play_img,
command=self.play)
self.play_button.grid(row=5, column=0)
self.run_app()
## Volume Controls
def volume_up_status(self):
if self.volume < 0.99:
self.volume_up_button["state"] = ACTIVE
else:
self.volume_up_button["state"] = DISABLED
self.after(1, self.volume_up_status)
def volume_down_status(self):
if self.volume > 0.01:
self.volume_down_button["state"] = ACTIVE
else:
self.volume_down_button["state"] = DISABLED
self.after(1, self.volume_down_status)
def increase_volume(self):
self.volume += 0.10
self.engine.setProperty("volume", self.volume)
print(self.volume)
def decrease_volume(self):
self.volume -= 0.10
self.engine.setProperty("volume", self.volume)
print(self.volume)
def mute_volume(self):
self.volume = 0
self.engine.setProperty("volume", self.volume)
## Speed Controls ##
def increase_rate(self):
self.rate += 25
self.engine.setProperty("rate", self.rate)
print(self.rate)
def decrease_rate(self):
self.rate -= 25
self.engine.setProperty("rate", self.rate)
print(self.rate)
## Voice Controls ##
def change_to_male(self):
self.engine.setProperty("voice", self.voices[0].id)
def change_to_female(self):
self.engine.setProperty("voice", self.voices[1].id)
## Play and Pause ##
def play(self):
self.engine.say("Hello and welcome to the DIY Audiobook Program.")
self.engine.runAndWait()
def run_app(self):
self.volume_up_status()
self.volume_down_status()
app = App()
app.mainloop()
Related
In the code below, I tried to develop an audio player that plays .wav files but unfortunately, the pause and play buttons don't work as intended. The pause button shows but it doesn't automatically start. The play button doesn't change even after it is clicked and it also does not play. The rewind and fast-forward button work by moving the slider. Unfortunately, after the initial period, it starts to malfunction. Affecting the volume_slider and giving me OSError: [Errno -9988] Stream closed. I'm still a beginner so please make your answers to be as simple as possible. Thank you.
import pyaudio
import tkinter as tk
from tkinter import Scale
from tkinter import PhotoImage
from tkinter import filedialog
file_path = filedialog.askopenfilename(filetypes=[("WAV files", "*.wav")])
CHUNK = 1024
p = pyaudio.PyAudio()
wf = wave.open(file_path, 'rb')
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
value = 0
def on_fast_forward(event):
global value, timer
seek_slider.set(value + 5)
timer = root.after(100, on_fast_forward, event)
print("Fast forward button clicked!")
def stop_fast_forward(event):
root.after_cancel(timer)
def on_rewind(event):
global value, timer
print("Rewind button clicked!")
seek_slider.set(value - 5)
timer = root.after(100, on_rewind, event)
def stop_rewind(self, event):
root.after_cancel(timer)
def on_play():
global data, value, play_button, pause_button
play_button.destroy()
if "!button3" not in root.children:
pause_button = tk.Button(root, image=play_img, command=on_pause)
pause_button.place(x=320, y=400)
stream.start_stream()
data = wf.readframes(CHUNK)
stream.write(data)
def on_pause():
global data, value, play_button, pause_button
pause_button.destroy()
if "!button2" not in root.children:
play_button = tk.Button(root, image=play_img, command=on_play)
play_button.place(x=320, y=400)
stream.stop_stream()
def on_volume_change(val):
global value
value = val
print("Seeked to:", value)
wf.setpos(int((value / wf.getnframes() * wf.getframerate()) * wf.getnframes())) # seek to the desired position in the audio file
data = wf.readframes(CHUNK) # read the next chunk of data
stream.write(data) # play the data
root = tk.Tk() # create the main window
root.title("Audio Transmitter") # set the title of the window
root.geometry("700x500") # set the window size to 700x500 pixels
# Create a scale widget
seek_slider = Scale(root, from_=0, to=int(wf.getnframes()/wf.getframerate()), orient='horizontal', command=lambda value: on_volume_change(int(value)))
seek_slider.place(x=45, y=350)
seek_slider.config(length=600)
seek_slider.config(showvalue=False)
seek_slider.set(0) # set the initial value of the slider to 100
# create rewind button
rewind_img = PhotoImage(file="Rewind_button.png")
rewind_button = tk.Button(root, image=rewind_img, command=on_rewind)
rewind_button.place(x=270, y=400)
rewind_button.bind("<Button-1>", on_rewind)
rewind_button.bind("<ButtonRelease-1>", stop_rewind)
# create play button
play_img = PhotoImage(file="Play_button.png")
play_button = tk.Button(root, image=play_img, command=on_play)
# create pause button
pause_img = PhotoImage(file="Pause_button.png")
pause_button = tk.Button(root, image=pause_img, command=on_pause)
pause_button.place(x=320, y=400)
# create fast forward button
fast_forward_img = PhotoImage(file="FastForward_button.png")
fast_forward_button = tk.Button(root, image=fast_forward_img, command=on_fast_forward)
fast_forward_button.place(x=370, y=400)
fast_forward_button.bind("<Button-1>", on_fast_forward)
fast_forward_button.bind("<ButtonRelease-1>", stop_fast_forward)
root.mainloop() # start the event loop
# create pause button
pause_img = PhotoImage(file="Pause_button.png")
pause_button = tk.Button(root, image=pause_img, command=on_pause)
pause_button.place(x=320, y=400)
# create fast forward button0
fast_forward_img = PhotoImage(file="FastForward_button.png")
fast_forward_button = tk.Button(root, image=fast_forward_img, command=on_fast_forward)
fast_forward_button.bind("<ButtonPress-1>", on_fast_forward)
fast_forward_button.bind("<ButtonRelease-1>", lambda event: setattr(fast_forward_flag, False))
fast_forward_button.place(x=370, y=400)
root.mainloop() # start the event loop
I'm trying to make it so that new information shows in in a new window, but I want the new window to be connected to the parent window, even when the parent window is clicked the new window should still show up similar to how a dropdown menu works. I'm also planning on having some of the new windows have treeviews later on.
from tkinter import *
win = Tk()
win.geometry("500x500+0+0")
def button_function ():
win2 = Toplevel()
label = Label(win2,text='dropdown', width=7)
label.pack()
win2.geometry(f"+{win.winfo_x()}+{win.winfo_y()+30}")
button = Button(win, command=lambda: button_function (), width=12)
button.pack()
win.mainloop()
Ok so with a little bit of googling I came across this post: tkinter-detecting-a-window-drag-event
In that post they show how you can keep track of when the window has moved.
By taking that code and making some small changes we can use the dragging() and stop_drag() functions to move the top level window back to where it was set to relative to the main window.
That said this will only work in this case. You will need to write something more dynamic to track any new windows you want so they are placed properly and on top of that you will probably want to build this in a class so you do not have to manage global variables.
With a combination of this tracking function and using lift() to bring the window up we get closer to what you are asking to do.
That said you will probably want remove the tool bar at the top of the root window to be more clean. I would also focus on using a dictionary or list to keep track of open and closed windows and their locations to make the dynamic part of this easier.
import tkinter as tk
win = tk.Tk()
win.geometry("500x500+0+0")
win2 = None
drag_id = ''
def dragging(event):
global drag_id
if event.widget is win:
if drag_id == '':
print('start drag')
else:
win.after_cancel(drag_id)
print('dragging')
drag_id = win.after(100, stop_drag)
if win2 is not None:
win2.lift()
win2.geometry(f"+{win.winfo_x()}+{win.winfo_y() + 30}")
def stop_drag():
global drag_id, win2, win
print('stop drag')
drag_id = ''
if win2 is not None:
win2.lift()
win2.geometry(f"+{win.winfo_x()}+{win.winfo_y() + 30}")
win.bind('<Configure>', dragging)
def button_function():
global win2
win2 = tk.Toplevel()
label = tk.Label(win2, text='drop down', width=7)
label.pack()
win2.geometry(f"+{win.winfo_x()}+{win.winfo_y()+30}")
tk.Button(win, command=button_function, width=12).pack()
win.mainloop()
EDIT:
Ok so I took some time to write this up in a class so you could see how it could be done. I have also added some level of dynamic building of the buttons and pop up windows.
We use a combination of lists and lambdas to perform a little bit of tracking and in the end we pull off exactly what you were asking for.
let me know if you have any questions.
import tkinter as tk
class Main(tk.Tk):
def __init__(self):
super().__init__()
self.geometry('500x500')
self.pop_up_list = []
self.drag_id = ''
self.button_notes = ['Some notes for new window', 'some other notes for new window', 'bacon that is all!']
self.bind('<Configure>', self.dragging)
for ndex, value in enumerate(self.button_notes):
print(ndex)
btn = tk.Button(self, text=f'Button {ndex+1}')
btn.config(command=lambda b=btn, i=ndex: self.toggle_button_pop_ups(i, b))
btn.grid(row=ndex, column=0, padx=5, pady=5)
self.pop_up_list.append([value, 0, None, btn])
def dragging(self, event):
if event.widget is self:
if self.drag_id == '':
pass
else:
self.after_cancel(self.drag_id)
self.drag_id = self.after(100, self.stop_drag)
for p in self.pop_up_list:
if p[1] == 1:
p[2].lift()
p[2].geometry(f"+{p[3].winfo_rootx() + 65}+{p[3].winfo_rooty()}")
def stop_drag(self):
self.drag_id = ''
for p in self.pop_up_list:
if p[1] == 1:
p[2].lift()
p[2].geometry(f"+{p[3].winfo_rootx() + 65}+{p[3].winfo_rooty()}")
def toggle_button_pop_ups(self, ndex, btn):
p = self.pop_up_list
if p[ndex][1] == 0:
p[ndex][1] = 1
p[ndex][2] = tk.Toplevel(self)
p[ndex][2].overrideredirect(1)
tk.Label(p[ndex][2], text=self.pop_up_list[ndex][0]).pack()
p[ndex][2].geometry(f"+{btn.winfo_rootx() + 65}+{btn.winfo_rooty()}")
else:
p[ndex][1] = 0
p[ndex][2].destroy()
p[ndex][2] = None
if __name__ == '__main__':
Main().mainloop()
I have created few windows using Tkinter. I need help in the implementation of switching from one window to another when the button has been clicked.
All windows that are created should have the same size.
And also I want to clear existing window data and show next window data.
If you want to have multiple windows opened and want to switch between each window with all of their widgets intact then I don't think destroying a window each time you switch is a good idea instead you can try to withdraw and deiconify the windows.
I've created something like this which can switch between windows and maintain the same geometry of the previous window as you said.
import tkinter as tk
class Window(tk.Toplevel):
# List to keep the reference of all the toplevel windows
_info_pages = []
def __init__(self, master=None, cnf={}, **kw):
kw = tk._cnfmerge( (cnf,kw) )
width = kw.pop('width', master.winfo_width()) # 250x250 will be the standard size of the window
height = kw.pop('height', master.winfo_height())
title = kw.pop('title', 'Win %s' %(len(self._info_pages)+1) )
super(Window, self).__init__(master=master, cnf=cnf, **kw)
for i in self._info_pages: i.wm_withdraw() # Hide the previous windows
if self._info_pages and width == master.winfo_width():
self.wm_geometry(self._info_pages[-1].winfo_geometry())
else:
self.wm_geometry("%dx%d+%d+%d" % (width, height,
master.winfo_rootx()+master.winfo_width(), master.winfo_rooty()))
self._info_pages.append(self)
self.title(title)
self.B1 = tk.Button(self, text='◀ Prev', padx=5, command=self.switch_to_prev)
self.B1.place(relx=0, rely=1, anchor='sw')
self.B2 = tk.Button(self, text='Next ▶', padx=5, command=self.switch_to_next)
self.B2.place(relx=1, rely=1, anchor='se')
self.enable_disable_button()
def enable_disable_button(self):
"""Enable and disable the buttons accordingly if there is no window."""
for i in self._info_pages:
if i == self._info_pages[0]: i.B1['state'] = 'disabled'
else: i.B1['state'] = 'normal'
if i == self._info_pages[-1]: i.B2['state'] = 'disabled'
else: i.B2['state'] = 'normal'
def switch_to_prev(self):
"""Switch to the previous window"""
index = self._info_pages.index(self)
if index != 0:
for i in self._info_pages:
i.wm_withdraw()
self._info_pages[index-1].geometry(self.winfo_geometry())
self._info_pages[index-1].wm_deiconify()
def switch_to_next(self):
"""Switch to the next window"""
index = self._info_pages.index(self)
if index+1 != len(self._info_pages):
for i in self._info_pages:
i.wm_withdraw()
self._info_pages[index+1].geometry(self.winfo_geometry())
self._info_pages[index+1].wm_deiconify()
def destroy(self):
"""if a window is destroyed this will open the last window in the list"""
self._info_pages.remove(self)
if self._info_pages:
self._info_pages[-1].geometry(self.winfo_geometry())
self._info_pages[-1].wm_deiconify()
self.enable_disable_button()
return super().destroy()
# This is just a demo
if __name__ == '__main__':
import random as rnd
root = tk.Tk()
root.geometry('250x250')
root.title("I'm the main window")
colorlist = ['beige','bisque','black','blanchedalmond','blue','blueviolet',
'burlywood', 'cadetblue','chartreuse','chocolate' ]
def create_window():
Window(root, bg=rnd.choice(colorlist))
tk.Button(root, text='Create Window', command=create_window).pack()
root.mainloop()
Getting back into programming after a 20 years hiatus. Been reading that the use of global variables in python is a sign of bad design, but can't figure out a better way of doing it.
Below is a small program that utilizes a global variable 'paused' to determine the state of the music player. This variable is utilized by a couple of functions.
Is there a better way of doing this without utilizing a global variable?
# Global variable to access from multiple functions
paused = False
def play_music():
global paused
if not paused:
try:
mixer.music.load(filename)
mixer.music.play()
statusBar['text'] = 'Playing Music - ' + os.path.basename(filename)
except:
tkinter.messagebox.showerror('File not found',
'Melody could not find the file.')
else:
mixer.music.unpause()
paused = False
statusBar['text'] = 'Playing Music - ' + os.path.basename(filename)
def stop_music():
mixer.music.stop()
statusBar['text'] = 'Music stopped'
def pause_music():
global paused
if not paused:
mixer.music.pause()
paused = True
statusBar['text'] = 'Music paused'
else:
play_music()
You could put all your functions inside a class, and make the "global" variable an attribute. In that way you can share it between methods:
class Player(object):
def __init__(self):
self.paused = False
def play_music(self):
if not self.paused:
# and so on
def pause_music(self):
if not self.paused:
# etc.
In case anyone else is interested, below is the improved code where a Player class was created to encapsulate the pause variable:
import os
from tkinter import *
from tkinter import filedialog
import tkinter.messagebox
from pygame import mixer
# Global variable to access from multiple functions
# paused = False
class Player:
def __init__(self):
self.paused = False
self.filename = None
def play_music(self):
if not self.paused:
try:
mixer.music.load(self.filename)
mixer.music.play()
statusBar['text'] = 'Playing Music - ' + os.path.basename(self.filename)
except FileNotFoundError:
tkinter.messagebox.showerror('File not found',
'Melody could not find the file. Please choose a music file to play')
else:
mixer.music.unpause()
self.paused = False
statusBar['text'] = 'Playing Music - ' + os.path.basename(self.filename)
#staticmethod
def stop_music():
mixer.music.stop()
statusBar['text'] = 'Music stopped'
def pause_music(self):
if not self.paused:
mixer.music.pause()
self.paused = True
statusBar['text'] = 'Music paused'
else:
self.play_music()
def rewind_music(self):
self.play_music()
statusBar['text'] = 'Music rewound'
#staticmethod
def set_volume(val):
# val is set automatically by the any tkinter widget
volume = int(val)/100 # mixer only takes value between 0 and 1
mixer.music.set_volume(volume)
# Create about us Message Box
#staticmethod
def about_us():
tkinter.messagebox.showinfo('About Melody', 'This is a music player built using python and tkinter')
def browse_file(self):
self.filename = filedialog.askopenfilename()
print(self.filename)
# Create main window
root = Tk()
# Create window frames
middle_frame = Frame(root)
bottom_frame = Frame(root)
# Create Menu
menu_bar = Menu(root)
root.config(menu=menu_bar)
# Create Player object
player = Player()
subMenu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label="File", menu=subMenu)
subMenu.add_command(label="Open", command=player.browse_file)
subMenu.add_command(label="Exit", command=root.destroy)
# it appears we can re-use subMenu variable and re-assign it
subMenu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label="Help", menu=subMenu)
subMenu.add_command(label="About Us", command=player.about_us)
# Initialise Mixer
mixer.init()
# Create and set the main window
root.title("Melody")
root.wm_iconbitmap(r'favicon.ico')
# root.geometry('300x300')
# Create and arrange widgets
text = Label(root, text="Lets make some noise!")
text.pack(pady=10)
middle_frame.pack(pady=30, padx=30) # Place the middle and bottom frame below this text
bottom_frame.pack()
playPhoto = PhotoImage(file='play-button.png')
playBtn = Button(middle_frame, image=playPhoto, command=player.play_music)
playBtn.grid(row=0, column=0, padx=10)
stopPhoto = PhotoImage(file='stop-button.png')
stopBtn = Button(middle_frame, image=stopPhoto, command=player.stop_music)
stopBtn.grid(row=0, column=1, padx=10)
pausePhoto = PhotoImage(file='pause-button.png')
pauseBtn = Button(middle_frame, image=pausePhoto, command=player.pause_music)
pauseBtn.grid(row=0, column=2, padx=10)
rewindPhoto = PhotoImage(file='rewind-button.png')
rewindBtn = Button(bottom_frame, image=rewindPhoto, command=player.rewind_music)
rewindBtn.grid(row=0, column=0, padx=20)
# Create and set volume slider
scale = Scale(bottom_frame, from_=0, to=100, orient=HORIZONTAL, command=player.set_volume)
scale.set(70) # set default slider and and volume
player.set_volume(70)
scale.grid(row=0, column=1, padx=10)
statusBar = Label(root, text='Welcome to Melody', relief=SUNKEN, anchor=W)
statusBar.pack(side=BOTTOM, fill=X)
# Keep main window displayed
root.mainloop()
Indeed, using global variable isn't recommended. You can have side effects that leads your program to have an unexpected behavior.
You have the possibility above (using class), but another solution is just to pass your variable as a parameter of your function.
def play_music(paused):
...
def stop_music(paused):
...
def pause_music(paused):
...
if you do not want to use classes, you could pass a settings dictionary that has a pause key, it will be mutated in all over your functions
def play_music(settings):
# some extra code
settings['pause'] = False
def stop_music(settings)
# some extra code
settings['pause'] = None
def pause_music(settings):
# some extra code
settings['pause'] = True
def main():
settings = {'pause': None}
play_music(settings)
.....
I need to make this clock open only after pressing a key, lets say "t". Now it opens immediately after running it.
import tkinter as tk
def update_timeText():
if (state):
global timer
timer[2] += 1
if (timer[2] >= 100):
timer[2] = 0
timer[1] += 1
if (timer[1] >= 60):
timer[0] += 1
timer[1] = 0
timeString = pattern.format(timer[0], timer[1], timer[2])
timeText.configure(text=timeString)
root.after(10, update_timeText)
def start():
global state
state=True
state = False
root = tk.Tk()
root.wm_title('Simple Kitchen Timer Example')
timer = [0, 0, 0]
pattern = '{0:02d}:{1:02d}:{2:02d}'
timeText = tk.Label(root, text="00:00:00", font=("Helvetica", 50))
timeText.pack()
startButton = tk.Button(root, text='Start', command=start)
startButton.pack()
update_timeText()
root.mainloop()
It is in another program so as I have my graphics window I will press "t" and the clock will open.
Keyboard is a python module that can detect keystrokes. Install it by doing this command.
pip install keyboard
Now you can do this.
while True:
try:
if keyboard.is_pressed('t'):
state = True
elif(state != True):
pass
except:
state = False
break #a key other than t the loop will break
I would recommend you to organize the code little bit, like class structure. One possible implementation would be like that:
import tkinter as tk
TIMER = [0, 0, 0]
PATTERN = '{0:02d}:{1:02d}:{2:02d}'
class Timer:
def __init__(self, master):
#I init some variables
self.master = master
self.state = False
self.startButton = tk.Button(root, text='Start', command=lambda: self.start())
self.startButton.pack()
self.timeText = tk.Label(root, text="00:00:00", font=("Helvetica", 50))
self.timeText.pack()
def start(self):
self.state = True
self.update_timeText()
def update_timeText(self):
if (self.state):
global TIMER
TIMER[2] += 1
if (TIMER[2] >= 100):
TIMER[2] = 0
TIMER[1] += 1
if (TIMER[1] >= 60):
TIMER[0] += 1
TIMER[1] = 0
timeString = PATTERN.format(TIMER[0], TIMER[1], TIMER[2])
self.timeText.configure(text=timeString)
self.master.after(10, self.update_timeText)
if __name__ == '__main__':
root = tk.Tk()
root.geometry("900x600")
root.title("Simple Kitchen Timer Example")
graph_class_object = Timer(master=root)
root.mainloop()
So clock will start when you click to button. If you want to start the clock by pressing "t" in keyboard, you need to bind that key to your function.
You can also add functionality if you want to stop the clock when you click to the button one more time.
EDIT:
if you also want to start to display the clock by clicking the button, you can move the code for initializing the label in to start function.
def start(self):
self.state = True
self.timeText = tk.Label(root, text="00:00:00", font=("Helvetica", 50))
self.timeText.pack()
self.update_timeText()