I am trying to make a game where a map Tk() window opens, the player chooses location, the map window closes, and the level window opens. When the player has a choice the leave the level and chooses 'yes', the level Tk() should close and the map should open back up so the player can click on a different location and open another Tk(). For some reason all Tks are opening at once. Here is my code.
class GUI_Control:
def __init__(self, player, delegate, level=-1):
self.delegate = delegate
self.player = player
self.current_level = level
self.map = Map(self)
self.current_level = level
#define level gui's here and put in data structure
hydra_level = Hydra_Level(self)
self.windows = [hydra_level]
def open(self):
if self.current_level == -1:
self.map.mainloop()
else:
self.current_level.mainloop()
def save(self):
self.delegate.save()
def swap_window(self, n):
#pull up the specified window
self.windows[n].mainloop()
class Map(Tk):
MAP_WIDTH = 600
MAP_HEIGHT = 375
def __init__(self, listener, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
self.listener = listener
# define map gui here
self.canvas = Canvas(self, width=self.MAP_WIDTH, height=self.MAP_HEIGHT)
self.canvas.pack()
self.map_picture = PhotoImage(file=r"images/archipelago.gif")
self.canvas.create_image(0, 0, image=self.map_picture)
def destroy(self, n=0):
Tk.destroy(self)
#send message back to gui_control to bring up another window
self.listener.swap_window(n)
class Hydra_Level(Tk):
def __init__(self, listener, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
self.listener = listener
def destroy(self):
Tk.destroy(self)
#bring up the map again by sending message back to the control
self.listener.open()
Both windows, the map and the level, open in the function GUI_Control.open(). Is there any way to make them open one at a time?
In a Tkinter/tkinter application, you must have only one Tk() instance running at the same time. So to resolve your problem, you can simply use Toplevel().
Related
This is a function as part of a class to create tkinter Toplevel instances. I am trying to have the X button on each window destroy itself and then create two new windows. Every time I try running this, 'test' is only printed once and only 1 new window will appear. Why is this happening? Thanks!
Here is the class for creating tkinter instances
class App(Toplevel):
nid = 0
def __init__(self, master, title, f, nid):
# Creates Toplevel to allow for sub-windows
Toplevel.__init__(self, master)
self.thread = None
self.f = f
self.nid = nid
self.master = master
self.canvas = None
self.img = None
self.label = None
self.title(title)
self.geometry('300x300')
def window(self):
# Creates play_sound thread for self
self.thread = threading.Thread(target=lambda: play_sound())
# Disables resizing
self.resizable(False, False)
self.img = PhotoImage(file=self.f)
# Creates canvas
self.canvas = Canvas(self, width=300, height=300)
self.canvas.create_image(20, 20, anchor=NW, image=self.img)
self.canvas.pack(fill=BOTH, expand=1)
# Function to move each window to a random spot within the screen bounds
def move(self):
while True:
new_x = random.randrange(0, x)
new_y = random.randrange(0, y)
cur_x = self.winfo_x()
cur_y = self.winfo_y()
dir_x = random.choice(['-', '+'])
dir_y = random.choice(['-', '+'])
# Tests if the chosen position is within the monitor
try:
if (eval(f'{cur_x}{dir_x}{new_x}') in range(0, x)
and eval(f'{cur_y}{dir_y}{new_y}') in range(0, y)):
break
# Prevents crashing if the program happens to exceed the recursion limit
except RecursionError:
pass
# Sets geometry to the new position
self.geometry(f"+{new_x}+{new_y}")
# Repeats every second
self.after(1000, self.move)
# Starts sound thread
def sound(self):
self.thread.start()
# Changes the function of the X button
def new_protocol(self, func):
def run():
#do_twice
def cmd():
print('test')
func()
def both():
self.destroy()
return cmd()
self.protocol('WM_DELETE_WINDOW', both)
run()
Here is the function to create new windows
def create_window():
global num
# Hides root window
root.withdraw()
# Creates a new window with variable name root{num}.
d['root{0}'.format(num)] = App(root, 'HE HE HE HA', r'build\king_image.png', 0)
app_list.append(d['root{0}'.format(num)].winfo_id())
print(d['root{0}'.format(num)])
globals().update(d)
# Starts necessary commands for window
d['root{0}'.format(num)].window()
d['root{0}'.format(num)].move()
d['root{0}'.format(num)].new_protocol(create_window)
d['root{0}'.format(num)].sound()
d['root{0}'.format(num)].mainloop()
num += 1
Here is the decorator function:
def do_twice(func):
#functools.wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper
If anyone needs any other parts of my code I will gladly edit this to include them.
I have a dynamic Screen which is generated based on a button you clicked on another screen. Issue is dat every time I enter the Screen, the buttons are regenerated and added to the existing buttons.
The reason is that I use the on_enter method, but I don't know how I can use on_kv_post for example, as these events happen on starting the app.
How can I initialise the screen every time I return to this screen?
class ClientEnvsGrid(Screen):
envProp = StringProperty('')
def __init__(self, **kwargs):
super(ClientEnvsGrid, self).__init__(**kwargs)
def on_enter(self, *args):
clientProp = self.manager.get_screen('clientlist').clientProp
try:
client_filepath = os.path.join('clients', clientProp, "environments.json")
client_file = open(client_filepath)
clientdata = json.loads(client_file.read())
print(clientdata)
self.ids.clientlabel.text = clientdata["clientname"]
for envs in clientdata["environments"]:
print(envs["name"])
envbutton = Button(text=envs["name"])
envbutton.bind(on_press=lambda *args: self.pressed('envbtn', *args))
self.ids.environments.add_widget(envbutton)
except:
print("No client data found")
self.manager.current = 'clientlist'
def pressed(self, instance, *args):
self.envProp = args[0].text
I've managed to fix it to include clear_widgets in the environments GridLayout in the on_leave event.
def on_leave(self, *args):
self.ids.environments.clear_widgets()
In the following code, when I press the button to add some secondary windows, and then try to close a window by using "Command-w" it does not always close the active window. But if I disable the menu creation by commenting the line self.gerar_menu(), windows are opened and closed as expected (I mean, by clicking the red 'x' button or by pressing Command-W in OS X). Any idea about what is wrong here?
Here is my current test code:
#!/usr/bin/env python3.6
# encoding: utf-8
import tkinter as tk
import tkinter.font
from tkinter import ttk
class baseApp(ttk.Frame):
"""
Parent classe for main app window (will include some aditional methods and properties).
"""
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.master = master
self.mainframe = ttk.Frame(master)
self.mainframe.pack()
class App(baseApp):
""" Base class for the main application window """
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.master = master
#self.gerar_menu() # This line breaks "Command-w" functionality
self.lbl_text = ttk.Label(self.mainframe, text="This is the Main Window")
self.lbl_text.pack()
self.btn = ttk.Button(self.mainframe, text="Open Second window",
command=lambda: self.create_detail_window(self, number=0))
self.btn.pack()
self.newDetailsWindow = {}
self.windows_count=0
def gerar_menu(self):
""" generate the application menu """
self.menu = tk.Menu(root)
root.config(menu=self.menu)
self.fileMenu = tk.Menu(self.menu)
self.menu.add_cascade(label="File", menu=self.fileMenu)
self.fileMenu.add_command(label="New Document", command=None, accelerator="Command+n")
def create_detail_window(self, *event, number=None):
self.windows_count += 1
self.newDetailsWindow[self.windows_count]=tk.Toplevel()
self.newDetailsWindow[self.windows_count].geometry('900x600+80+130')
self.newDetailsWindow[self.windows_count].title(f'Detail: {self.windows_count}')
self.newDetailsWindow[self.windows_count].wm_protocol("WM_DELETE_WINDOW", self.newDetailsWindow[self.windows_count].destroy)
self.newDetailsWindow[self.windows_count].bind("Command-w", lambda event: self.newDetailsWindow[-1].destroy())
self.detail_window = detailWindow(self.newDetailsWindow[self.windows_count], self.windows_count)
self.newDetailsWindow[self.windows_count].focus()
print(self.newDetailsWindow)
class detailWindow(ttk.Frame):
""" Base class for secondary windows """
def __init__(self, master, rep_num, *args,**kwargs):
super().__init__(master,*args,**kwargs)
self.num_rep = rep_num
self.master.minsize(900, 600)
self.master.maxsize(900, 600)
print(f"Showing details about nr. {self.num_rep}")
self.mainframe = ttk.Frame(master)
self.mainframe.pack()
self.lbl_text = ttk.Label(self.mainframe,
text=f"Showing details about nr. {self.num_rep}")
self.lbl_text.pack()
if __name__ == "__main__":
root = tk.Tk()
janela_principal = App(root)
root.title('Main Window')
root.bind_all("<Mod2-q>", exit)
root.mainloop()
If you want to create a binding for closing a window, you need the function to act upon the actual window that received the event. Your code is always deleting the last window that was opened no matter which window received the event.
The first step is to bind to a function rather than using lambda. While lambda has its uses, binding to a named function is much easier to debug and maintain.
Once the function is called, the event object can tell you which window got the event via the widget attribute of the event object. Given this window, you can get the toplevel window that this window is in (or itself, if it's a toplevel window) via the winfo_toplevel command.
For example:
window = tk.Toplevel(...)
...
window.bind("<Command-w>", self.destroy_window)
...
def destroy_window(self, event):
window = event.widget.winfo_toplevel()
window.destroy()
I'm writing a test program to detect mouse motion within a Tkinter Window using Python 2.*. I can create the necessary widgets and bind the appropriate event handler function to the root widget as needed:
import Tkinter as tk
class App:
def __init__(self, master=None):
self.root = master
self.frame = tk.Frame(master)
self.frame.pack()
self.create_widgets()
self.setup_handlers()
def create_widgets(self):
...
def setup_handlers(self):
self.root.bind('<Motion>', self.update) # This is the line I wish to look at
def update(self, event):
...
root = tk.Tk()
app = App(master=root)
root.mainloop()
What I want to do now is be able to activate the event handler with a combined input. I want to be able, for instance, to only activate the event handler when I move the mouse with the 'r' key held down. What event string would I need for this? Where can I find a full rundown on the event-string formatting for binding event handlers?
for the combined event handling, you can do something like:
class App:
holding = False
def __init__(self, master):
self.root = master
self.root.bind("<KeyPress>", self.holdkey)
self.root.bind("<KeyRelease>", self.releasekey)
def holdkey(self, e):
if e.char == "r" and not self.holding:
self.root.bind("<Motion>", self.combined_update)
self.holding = True
def releasekey(self, e):
if e.char == "r" and self.holding:
self.root.unbind("<Motion>")
self.holding = False
def combined_update(self, e):
# Event handling for the combined event.
print ("yo")
This will "probably" work.
There is a main window with menu and the progressbar. A correspondence window with OK button opens upon menu command and the OK button starts the process (here: 3 sec. sleep).
The correspondence window is created via inheritance from a class I have not provided here (If required for answer, please let me know). The methods apply and ok override existing methods in the mother class.
Now my problem: Since the progressbar sits in the main window (class App) and progressbar(start) and progressbar(stop) in the correspondence window I somehow have to pass (start) and (stop) via the mother class tkSimpleDialog.Dialog to class App. So I thought I also override the __init__(self..) method, provide self. to progressbar.
How can I make this work?
import Tkinter, ttk, tkFileDialog, tkSimpleDialog, time, threading
class App:
def __init__(self, master, progressbar):
self.progress_line(master)
def progress_line (self, master):
self.progressbar = ttk.Progressbar(master, mode='indeterminate')
self.progressbar.place(anchor = 'ne', height = "20", width = "150", x = "175", y = "30")
class AppMenu(object):
def __init__(self, master, progressbar):
self.master = master
self.menu_bar()
def menu_bar(self):
menu_bar = Tkinter.Menu(self.master)
self.menu_bar = Tkinter.Menu(self.master)
self.master.config(menu=self.menu_bar)
self.create_menu = Tkinter.Menu(self.menu_bar, tearoff = False)
self.create_menu.add_command(label = "do", command = self.do)
self.menu_bar.add_cascade(label = "now", menu = self.create_menu)
def do(self):
do1 = Dialog(self.master, progressbar)
class Dialog(tkSimpleDialog.Dialog):
def __init__(self, parent, progressbar):
tkSimpleDialog.Dialog.__init__(self, parent, progressbar)
self.transient(parent)
self.parent = parent
self.result = None
self.progressbar = progressbar
body = Frame(self)
self.initial_focus = self.body(body)
body.pack(padx=5, pady=5)
self.buttonbox()
self.grab_set()
if not self.initial_focus:
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.geometry("+%d+%d" % (parent.winfo_rootx()+50, parent.winfo_rooty()+50))
self.initial_focus.focus_set()
self.wait_window(self)
def ok(self, event=None):
self.withdraw()
self.start_foo_thread()
self.cancel()
def apply(self):
time.sleep(5)
def start_foo_thread(self):
global foo_thread
self.foo_thread = threading.Thread(target=self.apply)
self.foo_thread.daemon = True
self.progressbar.start()
self.foo_thread.start()
master.after(20, check_foo_thread)
def check_foo_thread(self):
if self.foo_thread.is_alive():
root.after(20, self.check_foo_thread)
else:
self.progressbar.stop()
master = Tkinter.Tk()
progressbar = None
app = App(master, progressbar)
appmenu = AppMenu(master, progressbar)
master.mainloop()
error messages:
first after clicking ok:
Exception in Tkinter callback
Traceback (most recent call last):
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk/Tkinter.py", line 1410, in __call__
File "ask-progressbar.py", line 57, in ok
self.start_foo_thread()
File "ask-progressbar.py", line 66, in start_foo_thread
self.progressbar.start()
AttributeError: Dialog2 instance has no attribute 'progressbar'
second: after closing app
Exception in Tkinter callback
Traceback (most recent call last):
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk/Tkinter.py", line 1410, in __call__
File "ask-progressbar.py", line 26, in do
do1 = Dialog2(self.master, progressbar)
File "ask-progressbar.py", line 33, in __init__
self.transient(parent)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk/Tkinter.py", line 1652, in wm_transient
TclError: can't invoke "wm" command: application has been destroyed
Below is a working version of your code. There were a number of issues I had to fix because you didn't change a number of things in the code from my answer to your other question about progressbars.
The answer to your main question here is basically that you have to pass the instance around and remember it when necessary in the various class instances involved so that their methods will have it available through theself argument when they need it. Also, the way you were trying to derive and override the tkSimpleDialog.Dialog base class methods was both over-complicated and incorrect as well.
Usually the best (and simplest) thing to do is just supply your own validate() and apply() methods since that's how it was designed to work. If you also need your own __init__() constructor, it's important to only pass parameters to the base class' method that it understands from within the one in the subclass. If you need more functionality, it can usually be provided via additional derived-class-only methods, that only it or other classes you've also created know about.
Anyway, here's what I ended-up with:
import Tkinter, ttk, tkFileDialog, tkSimpleDialog, time, threading
class App:
def __init__(self, master):
self.progress_line(master)
def progress_line(self, master):
# the value of "maximum" determines how fast progressbar moves
self._progressbar = ttk.Progressbar(master, mode='indeterminate',
maximum=4) # speed of progressbar
self._progressbar.place(anchor='ne', height="20", width="150",
x="175", y="30")
#property
def progressbar(self):
return self._progressbar # return value of private member
class AppMenu(object):
def __init__(self, master, progressbar):
self.master = master
self.menu_bar()
self.progressbar = progressbar
def menu_bar(self):
self.menu_bar = Tkinter.Menu(self.master)
self.master.config(menu=self.menu_bar)
self.create_menu = Tkinter.Menu(self.menu_bar, tearoff=False)
self.create_menu.add_command(label="do", command=self.do)
self.menu_bar.add_cascade(label="now", menu=self.create_menu)
def do(self):
Dialog(self.master, self.progressbar) # display the dialog box
class Dialog(tkSimpleDialog.Dialog):
def __init__(self, parent, progressbar):
self.progressbar = progressbar
tkSimpleDialog.Dialog.__init__(self, parent, title="Do foo?")
def apply(self):
self.start_foo_thread()
# added dialog methods...
def start_foo_thread(self):
self.foo_thread = threading.Thread(target=self.foo)
self.foo_thread.daemon = True
self.progressbar.start()
self.foo_thread.start()
master.after(20, self.check_foo_thread)
def check_foo_thread(self):
if self.foo_thread.is_alive():
master.after(20, self.check_foo_thread)
else:
self.progressbar.stop()
def foo(self): # some time-consuming function...
time.sleep(3)
master = Tkinter.Tk()
master.title("Foo runner")
app = App(master)
appmenu = AppMenu(master, app.progressbar)
master.mainloop()
Hope this helps.
Here's another, simpler, solution that doesn't require the use of threading -- so could be easier to use/adapt in your case. It calls the progressbar widget's update_idletasks() method multiple times during the time-consuming foo() function. Again, it illustrates how to pass the progressbar around to the various parts of the code that need it.
import Tkinter, ttk, tkFileDialog, tkSimpleDialog, time
class App:
def __init__(self, master):
self.progress_line(master)
def progress_line(self, master):
self._progressbar = ttk.Progressbar(master, mode='indeterminate')
self._progressbar.place(anchor='ne', height="20", width="150",
x="175", y="30")
#property
def progressbar(self):
return self._progressbar # return value of private member
class AppMenu(object):
def __init__(self, master, progressbar):
self.master = master
self.menu_bar()
self.progressbar = progressbar
def menu_bar(self):
self.menu_bar = Tkinter.Menu(self.master)
self.master.config(menu=self.menu_bar)
self.create_menu = Tkinter.Menu(self.menu_bar, tearoff=False)
self.create_menu.add_command(label="do foo", command=self.do_foo)
self.menu_bar.add_cascade(label="now", menu=self.create_menu)
def do_foo(self):
confirm = ConfirmationDialog(self.master, title="Do foo?")
self.master.update() # needed to completely remove conf dialog
if confirm.choice:
foo(self.progressbar)
class ConfirmationDialog(tkSimpleDialog.Dialog):
def __init__(self, parent, title=None):
self.choice = False
tkSimpleDialog.Dialog.__init__(self, parent, title=title)
def apply(self):
self.choice = True
def foo(progressbar):
progressbar.start()
for _ in range(50):
time.sleep(.1) # simulate some work
progressbar.step(10)
progressbar.update_idletasks()
progressbar.stop()
master = Tkinter.Tk()
master.title("Foo runner")
app = App(master)
appmenu = AppMenu(master, app.progressbar)
master.mainloop()