How to avoid using a global variable? - python

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

Related

Is there a way to create a second window that connects to the parent window like a dropdown menu

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

How to combine a blink function with a toggle function?

I have a function that refreshes a string. It has 2 buttons, on and off. If you push on it will print 'Test' multiple times per second.
def blink():
def run():
while (switch):
print('Test')
thread = threading.Thread(target=run)
thread.start()
def on():
global switch
switch = True
blink()
def off():
global switch
switch = False
I also have a toggle function that's is one button that toggles 'True' and 'False'. It displays 'Test' when True.
def toggle():
if button1.config('text')[-1] == 'False':
button1.config(text='True')
Label(root, text='Test').place(x=30, y=0, relheight=1, relwidth=0.7)
else:
button1.config(text='False')
Label(root, text='').place(x=30, y=0, relheight=1, relwidth=0.7)
How do I combine these 2? What I want is that instead of having an on/off button, I have one toggle-able button.
I tried making a class:
class Toggle:
def blink():
def run():
while (switch):
print('Test')
Label(root, text='Test').place(x=30, y=0, relheight=1, relwidth=0.7)
else:
Label(root, text='').place(x=30, y=0, relheight=1, relwidth=0.7)
thread = threading.Thread(target=run)
thread.start()
def toggle():
if button1.config('text')[-1] == 'False':
button1.config(text='True')
global switch
switch = True
blink()
else:
button1.config(text='False')
global switch
switch = False
But I get an error:
File "C:\Users\Daniel\PycharmProjects\stockalarm test\main.py", line 29
global switch
^
SyntaxError: name 'switch' is assigned to before global declaration
I tried looking into it but I cant figure out what to do.
As mentioned in the comments, if you use a class then switch should be an attribute of the class instead of being a global variable.
Additionally, what you did is more CLI oriented and what I suggest below is to use a more tkinter oriented approach.
You want a "toggle-able" button, which is, I think, like a tk.Checkbutton with the indicatoron option set to False.
Instead of using the switch global variable, you can use a tk.BooleanVar connected to the state of the button1 checkbutton.
This depends on what you actually want to do in the run() function but in your example using threading.Thread is an overkill. You can use tkinter .after(<delay in ms>, <callback>) method instead.
I have made the Toggle class inherit from tk.Frame to put both the label and toggle button inside. Here is the code:
import tkinter as tk
class Toggle(tk.Frame):
def __init__(self, master=None, **kw):
tk.Frame.__init__(self, master, **kw)
self.label = tk.Label(self)
self.switch = tk.BooleanVar(self)
self.button1 = tk.Checkbutton(self, text='False', command=self.toggle,
variable=self.switch, indicatoron=False)
self.label.pack()
self.button1.pack()
def blink(self):
if self.switch.get(): # switch is on
print('Test')
self.after(10, self.blink) # re-execute self.blink() in 10 ms
def toggle(self):
if self.switch.get(): # switch on
self.button1.configure(text='True') # set button text to True
self.label.configure(text='Test') # set label text to Test
self.blink() # start blink function
else: # switch off
self.button1.configure(text='False')
self.label.configure(text='')
root = tk.Tk()
Toggle(root).pack()
root.mainloop()

Pause and continue stopwatch

I am trying to create stopwatch. I have done it but I would like to pause and continue the time whenever I want. I have tried some things but I have no idea how to do it. Is there anybody who would explain me how to do it?
import time, tkinter
canvas=tkinter.Canvas(width=1900,height=1000,bg='white')
canvas.pack()
canvas.create_text(950,300,text=':',font='Arial 600')
def write(x_rec,y_rec,x_text,rep):
canvas.create_rectangle(x_rec,0,y_rec,750,outline='white',fill='white')
if rep<10:
canvas.create_text(x_text,400,text='0'+str(rep),font='Arial 600')
else:
canvas.create_text(x_text,400,text=str(rep),font='Arial 600')
def write_minutes(rep):
write(0,900,450,rep)
def write_seconds(rep):
write(1000,1900,1450,rep)
def time(num,remember):
while remember[0]<num:
remember[1]+=1
write_seconds(remember[1])
if remember[1]==60:
remember[0]+=1
remember[1]=0
write_seconds(remember[1])
write_minutes(remember[0])
canvas.update()
canvas.after(1000)
remember=[0,0]
num=1
write_seconds(remember[1])
write_minutes(remember[0])
time(5,remember)
I couldn't figure-out a clean way to modify your code to do what you want, so decided to implement the stop watch as a class to make the program more object-oriented and avoid the use of a bunch of global variables.
I haven't tested this thoroughly, but there's enough of it working to give you the idea. Note also that I changed a Resume button into one that toggles itself between that and being Pause button. This approach made adding a third one unnecessary.
Update
I noticed what could be potential problem because more and more objects keep getting added to the Canvas as the display is updated. This shouldn't be a problem for a short-running StopWatch instance, but might cause issues with a long-running one.
To avoid this, I modified the code to update the existing corresponding Canvas text object if there is one. I also moved the Buttons to the top, above the StopWatch.
from functools import partial
import time
import tkinter as tk
PAUSE, RESUME = 0, 1 # Button states.
# Button callback functions.
def _toggle(callback):
toggle_btn.state = 1 - toggle_btn.state # Toggle button state value.
toggle_btn.config(**toggle_btn_states[toggle_btn.state])
callback()
def _stop():
stopwatch.cancel_updates()
toggle_btn.config(state=tk.DISABLED)
stop_btn.config(state=tk.DISABLED)
class StopWatch:
def __init__(self, parent, run_time, width, height):
self.run_time = run_time
self.width, self.height = width, height
self.font = 'Arial 600'
self.canvas = tk.Canvas(parent, width=width, height=height, bg='white')
self.canvas.pack()
self.canvas.create_text(950, 300, text=':', font=self.font)
self.running, self.paused = False, False
self.after_id = None
def start(self):
self.elapsed_time = 0 # In seconds.
self._display_time()
self.after_id = self.canvas.after(1000, self._update)
self.running, self.paused = True, False
def _update(self):
if self.running and not self.paused:
if self.elapsed_time == self.run_time:
_stop() # Sets self.running to False.
self.canvas.bell() # Beep.
else:
self.elapsed_time += 1
self._display_time()
if self.running: # Keep update process going.
self.after_id = self.canvas.after(1000, self._update)
def _display_time(self):
mins, secs = divmod(self.elapsed_time, 60)
self._write_seconds(secs)
self._write_minutes(mins)
def _write_minutes(self, mins):
self._write(0, 900, 450, 'mins', mins)
def _write_seconds(self, secs):
self._write(1000, 1900, 1450, 'secs', secs)
def _write(self, x_rec, y_rec, x_text, tag, value):
text = '%02d' % value
# Update canvas text widget if it has non-empty text.
if self.canvas.itemcget(tag, 'text'):
self.canvas.itemconfigure(tag, text=text)
else: # Otherwise create it.
self.canvas.create_text(x_text, 400, text=text, tag=tag, font=self.font)
def pause_updates(self):
if self.running:
self.paused = True
def resume_updates(self):
if self.paused:
self.paused = False
def cancel_updates(self):
self.running, self.paused = False, False
if self.after_id:
self.canvas.after_cancel(self.after_id)
self.after_id = None
# main
root = tk.Tk()
# Create a Frame for Buttons (allows row of them to be centered).
button_frame = tk.Frame(root)
button_frame.pack(side=tk.TOP)
# Create StopWatch and configure buttons to use it.
stopwatch = StopWatch(root, 5, 1900, 1000)
toggle_btn = tk.Button(button_frame)
toggle_btn_states = {}
# Dictionary mapping state to button configuration.
toggle_btn_states.update({
PAUSE: dict(
text='Pause', bg='red', fg='white',
command=partial(_toggle, stopwatch.pause_updates)),
RESUME: dict(
text='Resume', bg='green', fg='white',
command=partial(_toggle, stopwatch.resume_updates))
})
toggle_btn.state = PAUSE
toggle_btn.config(**toggle_btn_states[toggle_btn.state])
toggle_btn.pack(side=tk.LEFT, padx=2)
stop_btn = tk.Button(button_frame, text='Stop', bg='blue', fg='white', command=_stop)
stop_btn.pack(side=tk.LEFT, padx=2)
stopwatch.start()
root.mainloop()
Here's a screenshot showing the stopwatch running:

How to make timer/program open only after pressing key instead of immediately?

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

Correct way to implement a custom popup tkinter dialog box

I just started learning how to create a custom pop up dialog box; and as it turns out, the tkinter messagebox is really easy to use, but it also does not do too much. Here is my attempt to create a dialog box that will take input and then store that in the username.
My question is what is the recommended style to implement this? As Bryan Oakley suggested in this comment.
I would advise against using a global variable. Instead of having the dialog destroy itself, have it destroy only the actual widget but leave the object alive. Then, call something like inputDialog.get_string() and then del inputDialog from your main logic.
Maybe using the global variable to return my string is not the best idea, but why? And what is the suggested way? I get confused because I don't know how to trigger the getstring once the window is destroyed, and... the line about destroying the actual widget, I am not sure if he is referring to TopLevel.
The reason I ask is because I want the pop up box to be destroyed after I press the submit button; because after all, I want it to resume back to the main program, update something, etc. What should the button method send do in this case? Because the idea in this particular example is to allow the user to do it over and over, if he desires.
import tkinter as tk
class MyDialog:
def __init__(self, parent):
top = self.top = tk.Toplevel(parent)
self.myLabel = tk.Label(top, text='Enter your username below')
self.myLabel.pack()
self.myEntryBox = tk.Entry(top)
self.myEntryBox.pack()
self.mySubmitButton = tk.Button(top, text='Submit', command=self.send)
self.mySubmitButton.pack()
def send(self):
global username
username = self.myEntryBox.get()
self.top.destroy()
def onClick():
inputDialog = MyDialog(root)
root.wait_window(inputDialog.top)
print('Username: ', username)
username = 'Empty'
root = tk.Tk()
mainLabel = tk.Label(root, text='Example for pop up input box')
mainLabel.pack()
mainButton = tk.Button(root, text='Click me', command=onClick)
mainButton.pack()
root.mainloop()
Using the global statement is unnecessary in the two scenarios that come to mind.
you want to code a dialog box that can be imported to use with a main GUI
you want to code a dialog box that can be imported to use without a main GUI
code a dialog box that can be imported to use with a main GUI
Avoiding the global statement can be accomplished by passing a dictionary & key when you create an instance of a dialog box. The dictionary & key can then be associated with the button's command, by using lambda. That creates an anonymous function that will execute your function call (with args) when the button is pressed.
You can avoid the need to pass the parent every time you create an instance of the dialog box by binding the parent to a class attribute (root in this example).
You can save the following as mbox.py in your_python_folder\Lib\site-packages or in the same folder as your main GUI's file.
import tkinter
class Mbox(object):
root = None
def __init__(self, msg, dict_key=None):
"""
msg = <str> the message to be displayed
dict_key = <sequence> (dictionary, key) to associate with user input
(providing a sequence for dict_key creates an entry for user input)
"""
tki = tkinter
self.top = tki.Toplevel(Mbox.root)
frm = tki.Frame(self.top, borderwidth=4, relief='ridge')
frm.pack(fill='both', expand=True)
label = tki.Label(frm, text=msg)
label.pack(padx=4, pady=4)
caller_wants_an_entry = dict_key is not None
if caller_wants_an_entry:
self.entry = tki.Entry(frm)
self.entry.pack(pady=4)
b_submit = tki.Button(frm, text='Submit')
b_submit['command'] = lambda: self.entry_to_dict(dict_key)
b_submit.pack()
b_cancel = tki.Button(frm, text='Cancel')
b_cancel['command'] = self.top.destroy
b_cancel.pack(padx=4, pady=4)
def entry_to_dict(self, dict_key):
data = self.entry.get()
if data:
d, key = dict_key
d[key] = data
self.top.destroy()
You can see examples that subclass TopLevel and tkSimpleDialog (tkinter.simpledialog in py3) at effbot.
It's worth noting that ttk widgets are interchangeable with the tkinter widgets in this example.
To accurately center the dialog box read → this.
Example of use:
import tkinter
import mbox
root = tkinter.Tk()
Mbox = mbox.Mbox
Mbox.root = root
D = {'user':'Bob'}
b_login = tkinter.Button(root, text='Log in')
b_login['command'] = lambda: Mbox('Name?', (D, 'user'))
b_login.pack()
b_loggedin = tkinter.Button(root, text='Current User')
b_loggedin['command'] = lambda: Mbox(D['user'])
b_loggedin.pack()
root.mainloop()
code a dialog box that can be imported to use without a main GUI
Create a module containing a dialog box class (MessageBox here). Also, include a function that creates an instance of that class, and finally returns the value of the button pressed (or data from an Entry widget).
Here is a complete module that you can customize with the help of these references: NMTech & Effbot.
Save the following code as mbox.py in your_python_folder\Lib\site-packages
import tkinter
class MessageBox(object):
def __init__(self, msg, b1, b2, frame, t, entry):
root = self.root = tkinter.Tk()
root.title('Message')
self.msg = str(msg)
# ctrl+c to copy self.msg
root.bind('<Control-c>', func=self.to_clip)
# remove the outer frame if frame=False
if not frame: root.overrideredirect(True)
# default values for the buttons to return
self.b1_return = True
self.b2_return = False
# if b1 or b2 is a tuple unpack into the button text & return value
if isinstance(b1, tuple): b1, self.b1_return = b1
if isinstance(b2, tuple): b2, self.b2_return = b2
# main frame
frm_1 = tkinter.Frame(root)
frm_1.pack(ipadx=2, ipady=2)
# the message
message = tkinter.Label(frm_1, text=self.msg)
message.pack(padx=8, pady=8)
# if entry=True create and set focus
if entry:
self.entry = tkinter.Entry(frm_1)
self.entry.pack()
self.entry.focus_set()
# button frame
frm_2 = tkinter.Frame(frm_1)
frm_2.pack(padx=4, pady=4)
# buttons
btn_1 = tkinter.Button(frm_2, width=8, text=b1)
btn_1['command'] = self.b1_action
btn_1.pack(side='left')
if not entry: btn_1.focus_set()
btn_2 = tkinter.Button(frm_2, width=8, text=b2)
btn_2['command'] = self.b2_action
btn_2.pack(side='left')
# the enter button will trigger the focused button's action
btn_1.bind('<KeyPress-Return>', func=self.b1_action)
btn_2.bind('<KeyPress-Return>', func=self.b2_action)
# roughly center the box on screen
# for accuracy see: https://stackoverflow.com/a/10018670/1217270
root.update_idletasks()
xp = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
yp = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
geom = (root.winfo_width(), root.winfo_height(), xp, yp)
root.geometry('{0}x{1}+{2}+{3}'.format(*geom))
# call self.close_mod when the close button is pressed
root.protocol("WM_DELETE_WINDOW", self.close_mod)
# a trick to activate the window (on windows 7)
root.deiconify()
# if t is specified: call time_out after t seconds
if t: root.after(int(t*1000), func=self.time_out)
def b1_action(self, event=None):
try: x = self.entry.get()
except AttributeError:
self.returning = self.b1_return
self.root.quit()
else:
if x:
self.returning = x
self.root.quit()
def b2_action(self, event=None):
self.returning = self.b2_return
self.root.quit()
# remove this function and the call to protocol
# then the close button will act normally
def close_mod(self):
pass
def time_out(self):
try: x = self.entry.get()
except AttributeError: self.returning = None
else: self.returning = x
finally: self.root.quit()
def to_clip(self, event=None):
self.root.clipboard_clear()
self.root.clipboard_append(self.msg)
and:
def mbox(msg, b1='OK', b2='Cancel', frame=True, t=False, entry=False):
"""Create an instance of MessageBox, and get data back from the user.
msg = string to be displayed
b1 = text for left button, or a tuple (<text for button>, <to return on press>)
b2 = text for right button, or a tuple (<text for button>, <to return on press>)
frame = include a standard outerframe: True or False
t = time in seconds (int or float) until the msgbox automatically closes
entry = include an entry widget that will have its contents returned: True or False
"""
msgbox = MessageBox(msg, b1, b2, frame, t, entry)
msgbox.root.mainloop()
# the function pauses here until the mainloop is quit
msgbox.root.destroy()
return msgbox.returning
After mbox creates an instance of MessageBox it starts the mainloop,
which effectively stops the function there until the mainloop is exited via root.quit().
The mbox function can then access msgbox.returning, and return its value.
Example:
user = {}
mbox('starting in 1 second...', t=1)
user['name'] = mbox('name?', entry=True)
if user['name']:
user['sex'] = mbox('male or female?', ('male', 'm'), ('female', 'f'))
mbox(user, frame=False)
Since the object inputDialog is not destroyed, I was able to access the object attribute. I added the return string as an attribute:
import tkinter as tk
class MyDialog:
def __init__(self, parent):
top = self.top = tk.Toplevel(parent)
self.myLabel = tk.Label(top, text='Enter your username below')
self.myLabel.pack()
self.myEntryBox = tk.Entry(top)
self.myEntryBox.pack()
self.mySubmitButton = tk.Button(top, text='Submit', command=self.send)
self.mySubmitButton.pack()
def send(self):
self.username = self.myEntryBox.get()
self.top.destroy()
def onClick():
inputDialog = MyDialog(root)
root.wait_window(inputDialog.top)
print('Username: ', inputDialog.username)
root = tk.Tk()
mainLabel = tk.Label(root, text='Example for pop up input box')
mainLabel.pack()
mainButton = tk.Button(root, text='Click me', command=onClick)
mainButton.pack()
root.mainloop()
Instead of using messagebox, you can use simpledialog. It is also part of tkinter. It is like a template instead of completely defining your own class. The simpledialog solves the problem of having to add the 'Ok' and 'Cancel' buttons yourself. I myself have ran into this problem and java2s has a good example on how to use simple dialog to make custom dialogs. This is their example for a two text field and two label dialog box. It is Python 2 though so you need to change it. Hope this helps :)
from Tkinter import *
import tkSimpleDialog
class MyDialog(tkSimpleDialog.Dialog):
def body(self, master):
Label(master, text="First:").grid(row=0)
Label(master, text="Second:").grid(row=1)
self.e1 = Entry(master)
self.e2 = Entry(master)
self.e1.grid(row=0, column=1)
self.e2.grid(row=1, column=1)
return self.e1 # initial focus
def apply(self):
first = self.e1.get()
second = self.e2.get()
print first, second
root = Tk()
d = MyDialog(root)
print d.result
Source: http://www.java2s.com/Code/Python/GUI-Tk/Asimpledialogwithtwolabelsandtwotextfields.htm
I used Honest Abe's 2nd part of the code titled:
code a dialog box that can be imported to use without a main GUI
as template and made some modifications. I needed a combobox instead of entry, so I also implemented it. If you need something else, it should be fairly easy to modify.
Following are the changes
Acts as a child
Modal to the parent
Centered on top of the parent
Not resizable
Combobox instead of entry
Click cross (X) to close the dialog
Removed
frame, timer, clipboard
Save the following as mbox.py in your_python_folder\Lib\site-packages or in the same folder as your main GUI's file.
import tkinter
import tkinter.ttk as ttk
class MessageBox(object):
def __init__(self, msg, b1, b2, parent, cbo, cboList):
root = self.root = tkinter.Toplevel(parent)
root.title('Choose')
root.geometry('100x100')
root.resizable(False, False)
root.grab_set() # modal
self.msg = str(msg)
self.b1_return = True
self.b2_return = False
# if b1 or b2 is a tuple unpack into the button text & return value
if isinstance(b1, tuple): b1, self.b1_return = b1
if isinstance(b2, tuple): b2, self.b2_return = b2
# main frame
frm_1 = tkinter.Frame(root)
frm_1.pack(ipadx=2, ipady=2)
# the message
message = tkinter.Label(frm_1, text=self.msg)
if cbo: message.pack(padx=8, pady=8)
else: message.pack(padx=8, pady=20)
# if entry=True create and set focus
if cbo:
self.cbo = ttk.Combobox(frm_1, state="readonly", justify="center", values= cboList)
self.cbo.pack()
self.cbo.focus_set()
self.cbo.current(0)
# button frame
frm_2 = tkinter.Frame(frm_1)
frm_2.pack(padx=4, pady=4)
# buttons
btn_1 = tkinter.Button(frm_2, width=8, text=b1)
btn_1['command'] = self.b1_action
if cbo: btn_1.pack(side='left', padx=5)
else: btn_1.pack(side='left', padx=10)
if not cbo: btn_1.focus_set()
btn_2 = tkinter.Button(frm_2, width=8, text=b2)
btn_2['command'] = self.b2_action
if cbo: btn_2.pack(side='left', padx=5)
else: btn_2.pack(side='left', padx=10)
# the enter button will trigger the focused button's action
btn_1.bind('<KeyPress-Return>', func=self.b1_action)
btn_2.bind('<KeyPress-Return>', func=self.b2_action)
# roughly center the box on screen
# for accuracy see: https://stackoverflow.com/a/10018670/1217270
root.update_idletasks()
root.geometry("210x110+%d+%d" % (parent.winfo_rootx()+7,
parent.winfo_rooty()+70))
root.protocol("WM_DELETE_WINDOW", self.close_mod)
# a trick to activate the window (on windows 7)
root.deiconify()
def b1_action(self, event=None):
try: x = self.cbo.get()
except AttributeError:
self.returning = self.b1_return
self.root.quit()
else:
if x:
self.returning = x
self.root.quit()
def b2_action(self, event=None):
self.returning = self.b2_return
self.root.quit()
def close_mod(self):
# top right corner cross click: return value ;`x`;
# we need to send it a value, otherwise there will be an exception when closing parent window
self.returning = ";`x`;"
self.root.quit()
It should be quick and easy to use. Here's an example:
from mbox import MessageBox
from tkinter import *
root = Tk()
def mbox(msg, b1, b2, parent, cbo=False, cboList=[]):
msgbox = MessageBox(msg, b1, b2, parent, cbo, cboList)
msgbox.root.mainloop()
msgbox.root.destroy()
return msgbox.returning
prompt = {}
# it will only show 2 buttons & 1 label if (cbo and cboList) aren't provided
# click on 'x' will return ;`x`;
prompt['answer'] = mbox('Do you want to go?', ('Go', 'go'), ('Cancel', 'cancel'), root)
ans = prompt['answer']
print(ans)
if ans == 'go':
# do stuff
pass
else:
# do stuff
pass
allowedItems = ['phone','laptop','battery']
prompt['answer'] = mbox('Select product to take', ('Take', 'take'), ('Cancel', 'cancel'), root, cbo=True, cboList=allowedItems)
ans = prompt['answer']
print(ans)
if (ans == 'phone'):
# do stuff
pass
elif (ans == 'laptop'):
# do stuff
pass
else:
# do stuff
pass
import tkinter
import tkinter.ttk as ttk
class MessageBox(object):
def __init__(self, msg, b1, b2, parent, cbo, cboList):
root = self.root = tkinter.Toplevel(parent)
root.title('Choose')
root.geometry('100x100')
root.resizable(False, False)
root.grab_set() # modal
self.msg = str(msg)
self.b1_return = True
self.b2_return = False
# if b1 or b2 is a tuple unpack into the button text & return value
if isinstance(b1, tuple): b1, self.b1_return = b1
if isinstance(b2, tuple): b2, self.b2_return = b2
# main frame
frm_1 = tkinter.Frame(root)
frm_1.pack(ipadx=2, ipady=2)
# the message
message = tkinter.Label(frm_1, text=self.msg)
if cbo: message.pack(padx=8, pady=8)
else: message.pack(padx=8, pady=20)
# if entry=True create and set focus
if cbo:
self.cbo = ttk.Combobox(frm_1, state="readonly", justify="center", values= cboList)
self.cbo.pack()
self.cbo.focus_set()
self.cbo.current(0)
# button frame
frm_2 = tkinter.Frame(frm_1)
frm_2.pack(padx=4, pady=4)
# buttons
btn_1 = tkinter.Button(frm_2, width=8, text=b1)
btn_1['command'] = self.b1_action
if cbo: btn_1.pack(side='left', padx=5)
else: btn_1.pack(side='left', padx=10)
if not cbo: btn_1.focus_set()
btn_2 = tkinter.Button(frm_2, width=8, text=b2)
btn_2['command'] = self.b2_action
if cbo: btn_2.pack(side='left', padx=5)
else: btn_2.pack(side='left', padx=10)
# the enter button will trigger the focused button's action
btn_1.bind('<KeyPress-Return>', func=self.b1_action)
btn_2.bind('<KeyPress-Return>', func=self.b2_action)
# roughly center the box on screen
# for accuracy see: https://stackoverflow.com/a/10018670/1217270
root.update_idletasks()
root.geometry("210x110+%d+%d" % (parent.winfo_rootx()+7,
parent.winfo_rooty()+70))
root.protocol("WM_DELETE_WINDOW", self.close_mod)
# a trick to activate the window (on windows 7)
root.deiconify()
def b1_action(self, event=None):
try: x = self.cbo.get()
except AttributeError:
self.returning = self.b1_return
self.root.quit()
else:
if x:
self.returning = x
self.root.quit()
def b2_action(self, event=None):
self.returning = self.b2_return
self.root.quit()
def close_mod(self):
# top right corner cross click: return value ;`x`;
# we need to send it a value, otherwise there will be an exception when closing parent window
self.returning = ";`x`;"
self.root.quit()
Tkinter simpledialog maybe useful for this problem.
Example usage
import tkinter as tk
name = tk.simpledialog.askstring("Title", "Message")
SOURCE:
https://python-course.eu/tkinter/dialogs-in-tkinter.php
There are different approaches available in tkiner
Waits
Buttontext
Body
class_
icon
baseclass
unresponsive root
MessageBox
True
False
False
False
True
False
True
Dialog
True
True
True
True
True
False
True
SimpleDialog
False
True
False
True
False
True
False
DialogClass
True
False
True
False
True
True
True
waits: waits for dialog to be destroyed.
buttontext: custom naming of Buttons
Body: intended to be costumized
class_: XSystem's may benefit from it.
icon: Custom icon
baseclass: intended as baseclass
All examples was initially wrote by Fredrik Lundh (R.I.P.) and can be found in the standard library
Dialog
import tkinter as tk
from tkinter.dialog import Dialog
def test():
d = Dialog(None, {'title': 'File Modified',
'text':
'File "Python.h" has been modified'
' since the last time it was saved.'
' Do you want to save it before'
' exiting the application.',
'bitmap': 'questhead',
'default': 0,
'strings': ('Save File',
'Discard Changes',
'Return to Editor')})
print(d.num)
root = tk.Tk()
tk.Button(root, text='Test', command=test).pack()
root.mainloop()
SimpleDialog
import tkinter as tk
from tkinter import simpledialog
class MessageBox(simpledialog.SimpleDialog):
def __init__(self, master,**kwargs):
simpledialog.SimpleDialog.__init__(self,master,**kwargs)
def done(self,num):
print(num)
self.num = num
self.root.destroy()
def test():
'SimpleDialog does not wait or return a result'
'You can retrieve the value by overwriting done or by MessageBox.num'
MessageBox(
root,title='Cancel',text='Im telling you!',class_=None,
buttons=['Got it!','Nah'], default=None, cancel=None)
root = tk.Tk()
tk.Button(root, text='Test', command=test).pack()
root.mainloop()
DialogClass
class MessageBox(simpledialog.Dialog):
def __init__(self, master,**kwargs):
simpledialog.Dialog.__init__(self,master,**kwargs)
def body(self, master):
'''create dialog body.
return widget that should have initial focus.
This method should be overridden, and is called
by the __init__ method.
'''
pass
def validate(self):
'''validate the data
This method is called automatically to validate the data before the
dialog is destroyed. By default, it always validates OK.
'''
return 1 # override
def apply(self):
'''process the data
This method is called automatically to process the data, *after*
the dialog is destroyed. By default, it does nothing.
'''
pass # override

Categories

Resources