How do you validate a window in Tkinter? - python

def createWidgets(self):
self.INSTRUCTIONS = Button(self) #creating button linked to instructions_window
self.INSTRUCTIONS["text"] = "Instructions"
self.INSTRUCTIONS["fg"] = "green"
self.INSTRUCTIONS["command"] = self.instruction_window #command which opens instructions_window
self.INSTRUCTIONS.pack({"side": "left"})
Currently, if I press the button multiple times then the instructions window will open multiple times. How do I ensure that when the button is pressed, if the window is already open then it will flash to show that the same window can't be opened. Is there a command? Or do I need to use a validation of some sort?

Here's a great article dealing with more complicated examples of dialog boxes.
Essentially what you are looking for is almost like a modal dialog window except it seems with the additional ability to still interact with the parent window to a degree. At this point it may be worth considering making it totally modal, but I do not know your use case. If not, you can definitely adapt the scripts given on the tutorial website to fit your needs.

The way I do this is to create a function that will create the window if it doesn't exist, and then display the window. Personally I don't think there's a need to flash the window, but you could do that if you want. Tkinter doesn't know how to flash a window, but you can do something simple like changing the colors briefly.
Here's an example:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.instruction_window = None
self.instructions = tk.Button(self, text="Instructions", foreground="green",
command=self.show_instructions)
self.instructions.pack(side="left")
def show_instructions(self):
'''show the instruction window; create it if it doesn't exist'''
if self.instruction_window is None or not self.instruction_window.winfo_exists():
self.instruction_window = InstructionWindow(self)
else:
self.instruction_window.flash()
class InstructionWindow(tk.Toplevel):
'''A simple instruction window'''
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.text = tk.Text(self, width=40, height=8)
self.text.pack(side="top", fill="both", expand=True)
self.text.insert("end", "these are the instructions")
def flash(self):
'''make the window visible, and make it flash temporarily'''
# make sure the window is visible, in case it got hidden
self.lift()
self.deiconify()
# blink the colors
self.after(100, lambda: self.text.configure(bg="black", fg="white"))
self.after(500, lambda: self.text.configure(bg="white", fg="black"))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()

Related

Unresponsive tkinter SimpleDialog box

Below is an outline of a tkinter GUI in which I want the same dialog box to be opened in various ways. The response selected by the user from choices in the dialog then needs to be returned to the mainloop.
The SimpleDialog class looks to be ideal for this and here I have just used the example provided in the dialog code. It is accessed by both the button and popup menu in the View class, along with their bindings in the Controller class.
It works just fine when called from the button, but when called from the popup menu (from a right click) the dialog appears and then freezes the entire app.
from tkinter import simpledialog as s
import tkinter as tk
class View(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self)
self.grid(row=0, column=0, sticky='nsew')
self.configure(bg = 'blue')
self.popup = tk.Menu(self, tearoff=0)
self.bind("<Button-2>", self.make_popup) #right-click to show popup
self.button = tk.Button(self, text='Test')
self.button.grid()
def make_popup(self, event):
try:
self.popup.tk_popup(event.x_root + 15, event.y_root, 0)
finally:
self.popup.grab_release()
class Controller():
def __init__(self, view):
view.popup.add_command(label ='do test', command = lambda : self.do_test(None, view))
view.popup.add_command(label ='dummy test', command = print('This one works OK'))
view.button.bind("<Button-1>", lambda e, : self.do_test(e, view))
def do_test(self, event, view):
d = s.SimpleDialog(view,
text="This is a test dialog. "
"Would this have been an actual dialog, "
"the buttons below would have been glowing "
"in soft pink light.\n"
"Do you believe this?",
buttons=["Yes", "No", "Cancel"],
default=0,
cancel=2,
title="Test Dialog")
print(d.go())
class App(tk.Tk):
def __init__(self):
super().__init__()
self.geometry('200x100')
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
view = View(self)
controller = Controller(view)
if __name__ == "__main__":
app = App()
app.mainloop()
It seems to me that the dialog should either work or not work, and not care how it is called. So I would be very grateful for an explanation as to why it responds in one case but not the other, and of course equally grateful for a fix.
The problem appears to lie in the print statement in the do_test callback, as splitting this into two lines fixes it
#print(d.go())
answer = d.go()
print(answer)
As reported in a comment this may be only an issue for MacOS (I am using MacOS 11.1 and Python 3.10.8 ).

How do you focus a window after launching another application in tkinter?

In my app, I have a callback that simply opens a file. The problem is that once python opens that file, my application loses focus. This behavior is going to really slow down my process. Here is an example of my issue:
import os
from tkinter import *
class App(Frame):
def __init__(self, *args, **kwargs):
Frame.__init__(self, *args, **kwargs)
Button(self, text='Open File', bg=self['bg'],
command=lambda: self.open_file('path_to_movie_or_PDF_or_anything_that_launches_application'),
relief=GROOVE).pack(padx=10, pady=10)
self.entry_field = Entry(self, bg=self['bg'], width=20)
self.entry_field.pack(padx=10, pady=10)
self.entry_field.focus()
def open_file(self, some_path):
os.startfile(some_path)
self.handle_focus()
def handle_focus(self):
# ATTEMPT 1
# self.master.after(1, lambda: self.master.focus_force())
# self.entry_field.focus()
# ATTEMPT 2
# self.master.attributes("-topmost", True)
# self.master.lift()
# self.entry_field.focus()
# ATTEMPT 3
# self.master.focus_set()
# self.entry_field.focus_set()
# ATTEMPT 4
# self.master.focus_force()
# self.master.lift()
# self.master.update()
# self.entry_field.focus()
pass
if __name__ == '__main__':
root = Tk()
root.config(bg='white')
App(root, bg='white').pack()
root.mainloop()
I am trying to open the file with the push button, then immediately turn the focus to the entry bar. How can this be achieved?
I have already looked to Tkinter main window focus for the answer, but focus_force() is not fixing the issue although it's function is exactly what I need. The documentation for focus_force() states that "Direct input focus to this widget even if the application does not have the focus. Use with caution!" For some reason, this does not work.
As you stated you wanted to set focus on the input field
inputfield.focus_set()
thats all

How to make pop-up window with force attention in Tkinter

I want to create a window which doesn't allow the user to access other windows until you give an input.
I tried win.attribute("ontop", True) but it allows the user to access other windows.
or is there any function like a force_focus_lock() in Tkinter python 3.8 which doesn't allow other window to
get focus until you give a input or the close present window.
I believe the below is what you are trying to do. Explanation is given in comments.
method #1: (PopOut1)
you can still move the main window
the new window assumes focus if there is a mouse release on main window
method #2: (PopOut2)
the main window is locked in place
the new window will assume focus, blink and "ding" if there is a mouse release on main window
import tkinter as tk
#first method
class PopOut1(tk.Toplevel):
def __init__(self, master, **kwargs):
tk.Toplevel.__init__(self, master, **kwargs)
self.geometry('400x300')
#set focus to this window
self.focus_set()
#releasing on any other tkinter window, within this process, forces focus back to this window
self.grab_set()
#second method
class PopOut2(tk.Toplevel):
def __init__(self, master, **kwargs):
tk.Toplevel.__init__(self, master, **kwargs)
self.geometry('400x300')
#set focus to this window
self.focus_set()
#disable the main window
master.attributes('-disabled', True)
#so this window can't end up behind the disabled window
#only necessary if this window is not transient
#self.attributes('-topmost', True)
#capture close event
self.protocol("WM_DELETE_WINDOW", self.close)
#event=None ~ in case you also want to bind this to something
def close(self, event=None):
#re-enable the main window
self.master.attributes('-disabled', False)
#destroy this window
self.destroy()
class App(tk.Tk):
TITLE = 'Application'
WIDTH, HEIGHT, X, Y = 800, 600, 50, 50
def __init__(self):
tk.Tk.__init__(self)
tk.Button(self, text="open popout 1", command=self.open1).grid()
tk.Button(self, text="open popout 2", command=self.open2).grid()
def open1(self):
PopOut1(self)
def open2(self):
#.transient(self) ~
# flash PopOut if focus is attempted on main
# automatically drawn above parent
# will not appear in taskbar
PopOut2(self).transient(self)
if __name__ == '__main__':
app = App()
app.title(App.TITLE)
app.geometry(f'{App.WIDTH}x{App.HEIGHT}+{App.X}+{App.Y}')
#app.resizable(width=False, height=False)
app.mainloop()

How can I correctly implement tkinter's .after() method in the context of my program?

I am creating a simple GUI program that utilizes Python and Tkinter to log the time/date when a user presses a button on the interface (by appending information to a .txt file), as well as sending an e-mail to a list of addresses informing the recipients that the log has been updated.
The program has three main frames/screens that I would like the user to navigate through. The navigation between the screens should be time-based. In other words, I would like the user to be redirected from the main screen to a secondary screen upon the press of a Tkinter button (which I have already established using the 'command' argument of the Tkinter widgets), and then be automatically redirected back to the main screen after a 5-second time delay.
I understand that using time.sleep() is not encouraged in GUI programs. However, I have had some trouble implementing Tkinter's .after() method, and haven't quite been able to achieve my desired result.
I have attached a simplified example of my program code that models my problem:
import tkinter as tk
class mainApplication(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
self.frames = {}
for F in (MainScreen, AcknowledgeScreen):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(MainScreen)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class MainScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Press Button to Log Time")
label.pack()
button = tk.Button(self, text="Confirm", command=lambda: controller.show_frame(AcknowledgeScreen))
button.pack()
class AcknowledgeScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Logging Completed. Please Wait.")
label.pack()
# The implementation below is giving me trouble.
self.after(5000, controller.show_frame(MainScreen))
root = mainApplication()
root.mainloop()
The other solutions I have attempted (for the line of interest) include:
# Attempt 1
self.after(5000, controller.show_frame(MainScreen)) # This code waits 5 seconds before opening the GUI window; does not redirect back to MainScreen from AcknowledgeScreen.
# Attempt 2
root.after(5000, controller.show_frame(MainScreen)) # This code throws an error 'NameError: name 'root' is not defined.
# Attempt 3
label.after(5000, controller.show_frame(MainScreen)) # This code waits 5 seconds before opening the GUI window; does not redirect back to MainScreen from AcknowledgeScreen.
Unfortunately, I have never been exposed to object-oriented programming before beginning with Tkinter, so I believe that my errors might be due to a fundamental misunderstanding of how OOP works. Nonetheless, I would appreciate if anyone could point me in the right direction or clarify my errors.
In mainApplication, both screens are initialized and their classes used for dictionary keys mapping to their instances.
From the ordering of your operations, the MainScreen should be raised in the stacking order after AcknowledgeScreen is displayed.
This operation shouldn't live in the AcknowledgeScreen.__init__ except you initialize the screens at the time they are needed.
You want to move this to MainScreen. You can refactor the MainScreen the following way.
class MainScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="Press Button to Log Time")
label.pack()
button = tk.Button(self, text="Confirm", command=self.confirm)
button.pack()
def confirm(self):
self.controller.show_frame(AcknowledgeScreen)
self.after(5000, self.back)
def back(self):
self.controller.show_frame(MainScreen)
after() (like bind() and command=) needs callback - it means function name without () and without arguments.
Use lambda
self.after(5000, lambda:controller.show_frame(MainScreen))
But I see different problem.
When you run program then it creates instances of all frames in mainApplication.__init__ so it runs also AcknowledgeScreen.__init__ at start - and it runs after() at start. It doesn't wait for your click.
BTW: because frames are create inside mainApplication.__init__ so they can't use root which will be created after mainApplication.__init__ ends its job.
You would have to add some parts in methods and run everytime when you click button.
class MainScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="Press Button to Log Time")
label.pack()
button = tk.Button(self, text="Confirm", command=self.change)
button.pack()
def change(self):
self.controller.show_frame(AcknowledgeScreen)
self.controller.frames[AcknowledgeScreen].change_after()
class AcknowledgeScreen(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="Logging Completed. Please Wait.")
label.pack()
def change_after(self):
# The implementation below is giving me trouble.
#both works
#root.after(5000, lambda:self.controller.show_frame(MainScreen))
self.after(5000, lambda:self.controller.show_frame(MainScreen))
Let's look at this code:
self.after(5000, controller.show_frame(MainScreen))
The above code does exactly the same thing as this:
result = controller.show_frame(MainScreen)
self.after(5000, result)
Notice what happens? The function controller.show_frame(MainScreen) executes immediately, rather than being executed by after.
Instead, you need to give after a callable -- roughly speaking, a reference to a function. If that function requires additional arguments, you can add those arguments when calling after:
self.after(5000, controller.show_frame, MainScreen)
The above code tells after to run controller.show_frame in five seconds, and to pass it the argument MainScreen when it is called.

Entry widget contents not changing with function call

I need to change the content of an entry whenever the tkinter frame is shown. Below is what I have so far, and it doesn't seem to work. I have tried to use data = self.read() and then now.insert(0, data) and that has not worked either. If the value is displayed then it doesn't get changed every time the class ReadLabel1 is called.
class ReadLabel1(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent, bg="blue")
label = tk.Label(self, text="SomeData:", font = "Times 12", bg="blue")
label.place(x=10, y=100) #ack(pady=5,padx=30)
self.smStr = tk.StringVar()
now=tk.Entry(self, width=22, textvariable=self.read())
now.place(x=120, y=103)
def read(self):
# code to get data
return data
You need to turn 'change the content of an entry' into a one parameter callback, turn 'whenever the tkinter frame is shown' into an event, and then bind together the app, the event, and the callback. Here is a minimal example.
import time
import tkinter as tk
root = tk.Tk()
now = tk.StringVar()
lab = tk.Label(root, textvariable=now)
lab.pack()
def display_now(event):
now.set(time.ctime())
root.bind('<Visibility>', display_now)
root.bind('<FocusIn>', display_now)
Minimizing the window to a icon and bringing it back up triggers the Visibility event. Covering and merely uncovering with a different window did not, at least not with Windows. Clicking on the uncovered, or merely inactivated, window triggered FocusIn. You can experiment more with your system. I used this tkinter reference

Categories

Resources