I'm creating some dialogs using TkInter and need to be able to open a child sub-window (modal or modeless) on clicking a button in the parent. The child would then allow a data record to be created and this data (either the record or if the operation was cancelled) needs to be communicated back to the parent window. So far I have:
import sel_company_dlg
from Tkinter import Tk
def main():
root = Tk()
myCmp = sel_company_dlg.SelCompanyDlg(root)
root.mainloop()
if __name__ == '__main__':
main()
This invokes the top level dialog which allows the user to select a company. The company selection dialog looks like this:
class SelCompanyDlg(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent_ = parent
self.frame_ = Frame( self.parent_ )
// .. more init stuff ..
self.btNew_ = Button( self.frame_, text="New ...", command=self.onNew )
def onNew(self):
root = Toplevel()
myCmp = company_dlg.CompanyDlg(root)
On clicking the New ... button, a Create Company dialog is displayed which allows the user to fill in company details and click on create or cancel. Here's the opening bit of that:
class CompanyDlg(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
// etc.
I'm struggling with the best way of invoking the child dialog in onNew() - what I have works but I'm not convinced it's the best approach and also, I can't see how to communicate the details to and from the child dialog.
I've tried looking at online tutorials / references but what I've found is either too simplistic or focuses on things like tkMessageBox.showinfo() which iss not what I want.
There are at least a couple ways to solve your problem. Either your dialog can directly send information to the main application, or your dialog can generate an event that tells the main application that data is really to be pulled from the dialog. If the dialog simply changes the appearance of something (for example, a font dialog) I usually generate an event. If the dialog creates or deletes data I typically have it push information back to the application.
I typically have an application object that acts as the controller for the GUI as a whole. Often this is the same class as the main window, or it can be a separate class or even defined as a mixin. This application object has methods that dialogs can call to feed data to the application.
For example:
class ChildDialog(tk.Toplevel):
def __init__(self, parent, app, ...)
self.app = app
...
self.ok_button = tk.Button(parent, ..., command=self.on_ok)
...
def on_ok(self):
# send the data to the parent
self.app.new_data(... data from this dialog ...)
class MainApplication(tk.Tk):
...
def on_show_dialog(self):
dialog = ChildDialog(self)
dialog.show()
def new_data(self, data):
... process data that was passed in from a dialog ...
When creating the dialog, you pass in a reference to the application object. The dialog then knows to call a specific method on this object to send data back to the application.
If you're not into the whole model/view/controller thing you can just as easily pass in a function rather than an object, effectively telling the dialog "call this function when you want to give me data".
In one of my projects I was trying to check within a child tk.Toplevel window (child1) of my root window (self), if a tk.Toplevel window (child2) was created by the user from within the root window, and if this window (child2) is present at the users screen at the moment.
If this wouldn't be the case, the new tk.Toplevel window should gets created by the child window (child1) of the root window, instead of the root window itself. And if it was already created by the root window and is currently present at the users screen, it should get focus() instead of getting reinitialized by "child1".
The root window was wrapped inside a class called App() and both "children" windows were created by methods inside the root class App().
I had to initialize "child2" in a quiet mode if an argument given to the method was True. I suppose that was the entangled mistake. The problem occurred on Windows 7 64 bit, if that's significant.
I tried this (example):
import tkinter as tk
from tkinter import ttk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
top = self.winfo_toplevel()
self.menuBar = tk.Menu(top)
top['menu'] = self.menuBar
self.menuBar.add_command(label='Child1', command=self.__create_child1)
self.menuBar.add_command(label='Child2', command=lambda: self.__create_child2(True))
self.TestLabel = ttk.Label(self, text='Use the buttons from the toplevel menu.')
self.TestLabel.pack()
self.__create_child2(False)
def __create_child1(self):
self.Child1Window = tk.Toplevel(master=self, width=100, height=100)
self.Child1WindowButton = ttk.Button(self.Child1Window, text='Focus Child2 window else create Child2 window', command=self.CheckForChild2)
self.Child1WindowButton.pack()
def __create_child2(self, givenarg):
self.Child2Window = tk.Toplevel(master=self, width=100, height=100)
if givenarg == False:
self.Child2Window.withdraw()
# Init some vars or widgets
self.Child2Window = None
else:
self.Child2Window.TestLabel = ttk.Label(self.Child2Window, text='This is Child 2')
self.Child2Window.TestLabel.pack()
def CheckForChild2(self):
if self.Child2Window:
if self.Child2Window.winfo_exists():
self.Child2Window.focus()
else:
self.__create_child2(True)
else:
self.__create_child2(True)
if __name__ == '__main__':
App().mainloop()
Here comes the problem:
I wasn't able to check if "child2" is already present. Got error: _tkinter.TclError: bad window path name
Solution:
The only way to get the right 'window path name' was, instead of calling the winfo_exists() method directly onto the "child2" window, calling the master of the "child1" window and adding the according attributes followed by the attributes of the master window you want to use.
Example (edit of the method CheckForChild2):
def CheckForChild2(self):
if self.Child2Window:
if self.Child1Window.master.Child2Window.winfo_exists():
self.Child1Window.master.Child2Window.focus()
else:
self.__create_child2(True)
else:
self.__create_child2(True)
Related
I use tkinter and CTK:
I have created a page for login and I want to stop or use this page when the user is logged in, I want to show a new window and I want to resume the window when I want? How can I do that, I didn't know how to make it
I'll bite. Here's an example application that opens a second window when the user clicks a button on the main window, and disables interaction with the main window until the second window is closed.
Some configuration has been omitted for brevity, and I'm not using CTk here because I don't know how you've implemented it in your specific application - however, it should be easy enough to modify this example to work with CTk.
import tkinter as tk
from tkinter import ttk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.open_button = ttk.Button(
self,
text='Open 2nd Window',
command=self.modal
)
self.open_button.pack()
def modal(self):
self.window = tk.Toplevel(self) # create new window
# bind a handler for when the user closes this window
self.window.protocol('WM_DELETE_WINDOW', self.on_close)
# disable interaction with the main (root) window
self.attributes('-disabled', True)
self.close_button = ttk.Button(
self.window,
text='Close Modal',
command=self.on_close
)
self.close_button.pack()
def on_close(self):
# re-enable interaction the root window
self.attributes('-disabled', False)
# close the modal window
self.window.destroy()
if __name__ == '__main__':
app = App()
app.mailoop() # run the app
In the future:
Provide code that shows you made a good-faith effort to solve the problem on your own
Don't post the same question multiple times within hours - if you need to make changes, edit the original question
If you mean you want to open up another window to do something before going back to the original window, you should consider using message box. Here is a link that goes over the types of messageboxes: https://docs.python.org/3/library/tkinter.messagebox.html.
I am opening other windows from a single tkinter button as shown here:
https://www.pythontutorial.net/tkinter/tkinter-toplevel/
The code shown there is
import tkinter as tk
from tkinter import ttk
class Window(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.geometry('300x100')
self.title('Toplevel Window')
ttk.Button(self,
text='Close',
command=self.destroy).pack(expand=True)
class App(tk.Tk):
def __init__(self):
super().__init__()
self.geometry('300x200')
self.title('Main Window')
# place a button on the root window
ttk.Button(self,
text='Open a window',
command=self.open_window).pack(expand=True)
def open_window(self):
window = Window(self)
window.grab_set()
if __name__ == "__main__":
app = App()
app.mainloop()
If I run this program, it is not possible to hit the "Open a window" button twice to get two Toplevel instances. I would like to get as many instances as I like to with only one button. Is this possible somehow?
Consider this line of code:
window.grab_set()
This is setting a grab on the first window that is created. That means that all events from both the keyboard and the mouse are funneled to the first window that is created. That means you can no longer click on the button in the root window until the grab has been removed. Note that if the window is destroyed, the grab is automatically removed.
Grabs are typically used when creating a modal dialog -- a dialog which requires user input before the program can continue. By doing a grab, you insure that the user can't interact with the main program until they've interacted with the dialog.
The solution is simple: remove the call to window.grab_set() if your goal is to be able to open multiple windows which can all be used at the same time.
Simply remove window.grab_set(). The grab_set() method routes all events for this application to this widget. Whatever events generated like button-click or keypress is directed to another window.
def open_window(self):
window = Window(self)
i have a main GUI-Window from which i open a new Window (FCT-popup) with a buttonclick:
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_MainWindow() # sets ui = to the main window from the ui-file
self.ui.setupUi(self)
[...]
def enter_fct_results(self):
self.FCTpopup = FCT_Window()
self.FCTpopup.show()
In the Window i have a QTable to fill and a button to submit the data and close the window:
class FCT_Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_FCT_Window()
self.ui.setupUi(self)
[...]
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
def on_submit(self): # event when user clicks
fct_nparray = np.zeros((self.ui.tableFCTinputs.rowCount(), self.ui.tableFCTinputs.columnCount()))
for j in range(self.ui.tableFCTinputs.columnCount()):
for i in range(self.ui.tableFCTinputs.rowCount()):
fct_nparray[i, j] = float(self.ui.tableFCTinputs.item(i, j).text())
return fct_nparray, lambda: self.close()
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
The receiving function iin the main window looks like ths:
def store_fct_data(self, data):
self.fct_data = data
Now i just want to understand how i can make either the mainwindow or the pushbutton which opens the 2nd window disabled. Disabling inside enter_fct_results() works, but if i want to enable it again with either store_fct_data or on_submit provides errors like this:
self.ui.pushButton_FCTresults.setEnabled(1)
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
AttributeError: type object 'MainWindow' has no attribute 'ui'
I dont think i have understood it here how to deal with multiple windows and stuff. For example how would i change a the color of a button in the main window by using a button in window2. How do i access the widgets? if i am inside the same Window i do that easily by
self.ui.pushbutton.setText("New Text")
I dont get how to access items and attributes across Windows :( Can you help me?
Access to attributes of another instance
There is a fundamental difference between disabling the button of the second window in enter_fct_results and what you tried in the lambda: in the first case, you're accessing an instance attribute (for instance, self.FCTpopup.ui.pushButton), while in the second you're trying to access a class attribute.
The self.ui object is created in the __init__ (when the class instance is created), so the instance will have an ui attribute, not the class:
class Test:
def __init__(self):
self.value = True
test = Test()
print(test.value)
>>> True
print(Test.value)
>>> AttributeError: type object 'Test' has no attribute 'value'
Provide a reference
The simple solution is to create a reference of the instance of the first window for the second:
def enter_fct_results(self):
self.FCTpopup = FCT_Window(self)
self.FCTpopup.show()
class FCT_Window(QMainWindow):
def __init__(self, mainWindow):
QMainWindow.__init__(self)
self.mainWindow = mainWindow
self.ui.pushButton_submitFCT.clicked.connect(self.doSomething)
def doSomething(self):
# ...
self.mainWindow.ui.pushButton.setEnabled(True)
Using modal windows (aka, dialogs)
Whenever a window is required for some temporary interaction (data input, document preview, etc), a dialog is preferred: the main benefit of using dialogs is that they are modal to the parent, preventing interaction on that parent until the dialog is closed; another benefit is that (at least on Qt) they also have a blocking event loop within their exec() function, which will only return as soon as the dialog is closed. Both of these aspects also make unnecessary disabling any button in the parent window. Another important reason is that QMainWindow is not intended for this kind of operation, also because it has features that generally unnecessary for that (toolbars, statusbars, menus, etc).
def enter_fct_results(self):
self.FCTpopup = FCT_Window(self)
self.FCTpopup.exec_()
class FCT_Window(QDialog):
def __init__(self, parent):
QMainWindow.__init__(self, parent)
self.ui.pushButton_submitFCT.clicked.connect(self.doSomething)
def doSomething(self):
# ...
self.accept()
The above makes mandatory to recreate the ui in designer using a QDialog (and not a QMainWindow) instead. You can just create a new one and drag&drop widgets from the original one.
i finally found my mistake: It was the place of the signal connection. It has to be right before the 2nd window is opened:
def enter_fct_results(self):
self.FCTpopup = Dialog(self.fct_data)
self.FCTpopup.submitted.connect(self.store_fct_data)
self.FCTpopup.exec_()
With this now i can send the stored data from the mainwindow to the opened window, import into the table, edit it and send it back to the main window on submit.
I know I can intercept pressing the X button with protocol("WM_DELETE_WINDOW", do_something) however I am having a hard time figuring out how to activate this button or at least the protocol that triggers when this button is pressed.
Here is the situation. I have 2 classes. My main Tk class and my Menu class. When I am setting up the command to close the program with an exit button from the menu I want this button to do exactly the same thing as the X button on the Tk class.
Now I know I could simply call the controller that was passed to the menu class and then call the method I built to handle the close event however I am trying to build this menu class in such a way that I do not need to do this from the menu class. This will allow me to use the menu class on any app I build with little to no editing.
I have not been able to find a post or some documentation that tells me how I can programmatically activate the "WM_DELETE_WINDOW" protocol.
Here is an image if it is unclear what I want. Simply I want the exit button to do exactly what the X button does.
Main class:
import tkinter as tk
import PIP_MENU
class PIP(tk.Tk):
def __init__(self):
super().__init__()
PIP_MENU.start(self)
self.protocol("WM_DELETE_WINDOW", self.handle_close)
def handle_close(self):
print("Closing")
self.quit()
if __name__ == '__main__':
PIP().mainloop()
Menu class on separate .py file:
import tkinter as tk
class Menu(tk.Menu):
def __init__(self, controller):
super().__init__()
self.controller = controller
controller.config(menu=self)
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Exit", command=self.handle_exit)
def handle_exit(self):
# What can I do here that will be handled by
# protocol "WM_DELETE_WINDOW" of the main class?
# All I can find is destroy() and quit()
# But both of these do not get handled by "WM_DELETE_WINDOW".
def start(controller):
Menu(controller)
I have not been able to find a post or some documentation that tells me how I can programmatically active the "WM_DELETE_WINDOW" protocol.
You can't. By definition, the WM_DELETE_WINDOW protocol comes from the window manager.
Catching the protocol handler is designed to give you an opportunity to override its behavior. It is not designed to be a way to trigger some code no matter how the application is destroyed. If you want to run some code when the window is destroyed, whether that is by the user clicking the control on the window frame or through some other way, the correct way to do that is to bind to the <Destroy> event on the root window.
You have to be careful, in that any binding on the root window will be triggered for every widget. Therefore, your binding should only run when event.widget is the same as the root window.
The following example illustrates the technique. There is a method handle_close which is called whenever the window is destroyed. Whether you close the window by clicking on the control on the window frame, or whether you click on the "Close me!" button, the code still runs.
import tkinter as tk
class Example(tk.Tk):
def __init__(self):
super().__init__()
self.bind("<Destroy>", self.handle_close)
button = tk.Button(self, text="Close me!", command=self.destroy)
button.pack()
def handle_close(self, event):
if event.widget == self:
print("Closing")
self.quit()
example = Example()
example.mainloop()
I don't believe there's a method that invokes a specific protocol, since protocol seems to be a specific event watch. Here's a snippet from the module's class Tk:
class Tk(Misc, Wm):
"""Toplevel widget of Tk which represents mostly the main window
of an application. It has an associated Tcl interpreter."""
def _loadtk(self):
...
self.protocol("WM_DELETE_WINDOW", self.destroy)
As you can see, by default the module itself sets the protocol to destroy(). The protocol() method only seeks to replace the default function (at the absence of a function, it just removes the function):
def wm_protocol(self, name=None, func=None):
"""Bind function FUNC to command NAME for this widget.
Return the function bound to NAME if None is given. NAME could be
e.g. "WM_SAVE_YOURSELF" or "WM_DELETE_WINDOW"."""
if callable(func):
command = self._register(func)
else:
command = func
return self.tk.call(
'wm', 'protocol', self._w, name, command)
protocol = wm_protocol
but to achieve what you want you should be able to reference back to the same handling method with this:
def handle_exit(self):
self.controller.handle_close()
Of course, this is not as versatile since you must explicitly know the handler in your main window.
Thought I have accepted Bryan's answer I did manage to come to a workaround I think is fine here.
If I pass the method that is being used to deal with window closing to my menu class and then check if something has been passed I can then decide on weather or not to use the exit method I made or self.controller.destroy() with an if statement.
Here is my solution.
Main file:
import tkinter as tk
from tkinter import messagebox
import PIP_MENU
class PIP(tk.Tk):
def __init__(self):
super().__init__()
PIP_MENU.start(self, self.handle_close)
self.protocol("WM_DELETE_WINDOW", self.handle_close)
def handle_close(self):
x = messagebox.askquestion("DERP", "Do you want to close without saving?")
if x == "yes":
self.destroy()
if __name__ == '__main__':
PIP().mainloop()
Menu file:
import tkinter as tk
class Menu(tk.Menu):
def __init__(self, controller, exit_handler=None):
super().__init__()
self.controller = controller
self.exit_handler = exit_handler
controller.config(menu=self)
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Exit", command=self.handle_exit)
def handle_exit(self):
if self.exit_handler != None:
self.exit_handler()
else:
self.controller.quit()
def start(controller, exit_handler=None):
return Menu(controller, exit_handler)
I'm looking for the most efficient way to create a child window and also forbid all access to the main window, in order that user will have to click on "OK" button inside the child window in order to recover the access to the main window.
Here is my code, using Toplevel class. It works, but is there a more efficient way to do ?
from tkinter import *
class MainWindow(Tk):
def __init__(self):
Tk.__init__(self)
def SetEnableStatus(self, status):
for w in self.winfo_children():
if status == False:
w.grab_set()
else:
w.grab_release()
def CreateChildWindow(self):
subWindow = Toplevel(self)
def quit_subwindow():
subWindow.destroy()
self.SetEnableStatus(True) # Enable all widgets of main window
Button(subWindow, text='Exit', command=quit_subwindow).pack()
self.SetEnableStatus(False) # Disable all widgets of main window
It should be enough to call grab_set on the Toplevel object, and when you are done with it, you can simply destroy it, and call grab_set on self (but I am not 100% sure, even if the resulting program below confirms it).
In fact, if you create a button on your Tk root window and if you associate with this button for example a lambda function that prints something, then nothing will be printed, after "setting the grab" on the child window.
See the following example where basically all events are redirected to the Toplevel window, instead of to the Tk root window:
from tkinter import *
class MainWindow(Tk):
def __init__(self):
Tk.__init__(self)
Entry(self).pack(side="left")
Button(self, text="whoami", command=lambda : print("A Tk root window.")).pack(side="left")
def CreateChildWindow(self):
subWindow = Toplevel(self)
def quit_subwindow():
subWindow.destroy()
self.grab_set()
Button(subWindow, text="whoami", command=lambda : print("A Toplevel child window.")).pack()
Button(subWindow, text='Exit', command=quit_subwindow).pack()
subWindow.grab_set()
win = MainWindow()
win.CreateChildWindow()
win.mainloop()
Check out the following article at effbot.org to know more about how to create dialogs:
http://effbot.org/tkinterbook/tkinter-dialog-windows.htm