gpt4 book ai didi

python - Tkinter:只允许一个 Toplevel 窗口实例

转载 作者:太空宇宙 更新时间:2023-11-04 05:23:06 24 4
gpt4 key购买 nike

我有一个带有多个窗口的 tkinter 程序。这是完整的代码,以备不时之需。

import tkinter as tk
import tkinter.scrolledtext as tkst
from tkinter import ttk
import logging
import time


def popupmsg(msg):
popup = tk.Toplevel()
popup.wm_title("!")
label = ttk.Label(popup, text=msg)
label.pack(side="top", fill="x", pady=10)
b1 = ttk.Button(popup, text="Okay", command=popup.destroy)
b1.pack()
popup.mainloop()


def test1():
root.logger.error("Test")


def toggle(self):
t_btn = self.t_btn
if t_btn.config('text')[-1] == 'Start':
t_btn.config(text='Stop')

def startloop():
if root.flag:
now = time.strftime("%c")
root.logger.error(now)
root.after(30000, startloop)
else:
root.flag = True
return
startloop()
else:
t_btn.config(text='Start')
root.logger.error("Loop stopped")
root.flag = False


class TextHandler(logging.Handler):

def __init__(self, text):
# run the regular Handler __init__
logging.Handler.__init__(self)
# Store a reference to the Text it will log to
self.text = text

def emit(self, record):
msg = self.format(record)

def append():
self.text.configure(state='normal')
self.text.insert(tk.END, msg + '\n')
self.text.configure(state='disabled')
# Autoscroll to the bottom
self.text.yview(tk.END)

# This is necessary because we can't modify the Text from other threads
self.text.after(0, append)

def create(self):
# Create textLogger
topframe = tk.Frame(root)
topframe.pack(side=tk.TOP)

st = tkst.ScrolledText(topframe, bg="#00A09E", fg="white", state='disabled')
st.configure(font='TkFixedFont')

st.pack()

self.text_handler = TextHandler(st)

# Add the handler to logger
root.logger = logging.getLogger()
root.logger.addHandler(self.text_handler)

def stop(self):
root.flag = False

def start(self):
if root.flag:
root.logger.error("error")
root.after(1000, self.start)
else:
root.logger.error("Loop stopped")
root.flag = True
return

def loop(self):
self.start()


class HomePage(tk.Frame):

def __init__(self, parent):
tk.Frame.__init__(self, parent)

container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)

self.menubar = tk.Menu(container)

# Create taskbar/menu
file = tk.Menu(self.menubar)
file.add_command(label="Run", command=lambda: test1())
file.add_command(label="Stop", command=lambda: test1())
file.add_separator()
file.add_command(label="Settings", command=lambda: Settings())
file.add_separator()
file.add_command(label="Quit", command=quit)
self.menubar.add_cascade(label="File", menu=file)

self.master.config(menu=self.menubar)

#logger and main loop
th = TextHandler("none")
th.create()
root.flag = True
root.logger.error("Welcome to ShiptScraper!")

bottomframe = tk.Frame(self)
bottomframe.pack(side=tk.BOTTOM)

topframe = tk.Frame(self)
topframe.pack(side=tk.TOP)

self.t_btn = tk.Button(text="Start", highlightbackground="#56B426", command=lambda: toggle(self))
self.t_btn.pack(pady=5)

self.exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
self.exitButton.pack()
root.setting = False


class Settings(tk.Toplevel):

def __init__(self, master=None):
tk.Toplevel.__init__(self, master)
self.wm_title("Settings")
print(Settings.state(self))

exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
exitButton.pack()


class Help(tk.Toplevel):

def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.wm_title("Help")

exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
exitButton.pack()


if __name__ == "__main__":
root = tk.Tk()
root.configure(background="#56B426")
root.wm_title("ShiptScraper")
app = HomePage(root)
app.mainloop()

基本上我的问题是每次单击菜单中的命令 Settings 都会弹出一个新的 Settings 窗口。我不知道如何制作它,以便它可以检测一个窗口实例是否已经打开。我尝试使用 state() 作为 HomePage 类中方法的检查

#in it's respective place as shown above
file.add_command(label="Settings", command=lambda: self.open(Settings))

#outside the init as a method
def open(self, window):
if window.state(self) != 'normal':
window()

这会返回这个错误

Exception in Tkinter callback
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/tkinter/__init__.py", line 1550, in __call__
return self.func(*args)
File "/Users/user/pythonProjects/ShiptScraper/ShiptScraperGUI.py", line 112, in <lambda>
file.add_command(label="Settings", command=lambda: self.open(Settings))
File "/Users/user/pythonProjects/ShiptScraper/ShiptScraperGUI.py", line 139, in open
if window.state(self) != 'normal':
File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/tkinter/__init__.py", line 1826, in wm_state
return self.tk.call('wm', 'state', self._w, newstate)
_tkinter.TclError: window ".4319455216" isn't a top-level window

我已经尝试使用 winfo_exists() 方法,但似乎除非我已经销毁了窗口(如果它还没有打开我就没有销毁)这会对我有什么好处。尽管如此,这是我尝试过的组合之一

def open(self, window):
if window.winfo_exists(self) != 1:
window()

这当然什么都不做。我不会经历所有其他错误的组合。我已经尝试过了,因为老实说,此时我无法记住它们。

我也曾尝试将这些 open 方法定义为任何类之外的函数,但它们在那里也不起作用,通常是因为未定义 self 关键字的混淆在类之外,但需要成为 winfo_exists()state() 方法的参数。

我还认为我的问题是,在将这些函数用作 HomePage 类中的方法时,是因为每当我引用 self 时,它都会检查 HomePage,不是我在方法中作为参数传递的任何窗口。不过我不太确定,这就是我来这里的原因。

实际上,我想做的只是在我的 HomePage 窗口中创建一个标准方法来控制菜单(以及稍后可能的按钮)如何打开一个窗口。这在逻辑上(在我自己的伪代码中)是:

def open(window)
if window does not exist:
open an instance of window

这可能吗,或者我应该采用更好的窗口管理方法吗?

编辑:我最初没有提到我的操作系统是运行 Mavericks 的 Mac OSX。显然这可能是一个 OSX 问题。此外,如果您要对这个问题投反对票,至少要发表评论并告诉我为什么/如何修改它以使其变得更好。

我现在已经尝试了这些组合

class Settings(tk.Toplevel):

def __init__(self, master=None):
tk.Toplevel.__init__(self, master)
self.wm_title("Settings")
# added grab_set()
self.grab_set()
#
print(Settings.state(self))

exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
exitButton.pack()

class Settings(tk.Toplevel):

def __init__(self, master=None):
tk.Toplevel.__init__(self, master)
self.wm_title("Settings")
# added grab_set()
self.grab_set()
self.focus()
#
print(Settings.state(self))

exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
exitButton.pack()

class Settings(tk.Toplevel):

def __init__(self, master=None):
tk.Toplevel.__init__(self, master)
self.wm_title("Settings")
# added grab_set()
self.attributes("-topmost", True)
#
print(Settings.state(self))

exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
exitButton.pack()

类设置(tk.Toplevel):

def __init__(self, master=None):
tk.Toplevel.__init__(self, master)
self.wm_title("Settings")
# added grab_set()
self.after(1, lambda: self.focus_force())

#
print(Settings.state(self))

exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
exitButton.pack()

编辑#2:

我想出了一个解决方法……我讨厌它。但它有效,至少目前如此。我绝对仍然希望有更好的解决方案。

import tkinter as tk
import tkinter.scrolledtext as tkst
from tkinter import ttk
import logging
import time



def popupmsg(msg):
popup = tk.Toplevel()
popup.wm_title("!")
label = ttk.Label(popup, text=msg)
label.pack(side="top", fill="x", pady=10)
b1 = ttk.Button(popup, text="Okay", command=popup.destroy)
b1.pack()
popup.mainloop()


def test1():
root.logger.error("Test")


def toggle(self):
t_btn = self.t_btn
if t_btn.config('text')[-1] == 'Start':
t_btn.config(text='Stop')

def startloop():
if root.flag:
now = time.strftime("%c")
root.logger.error(now)
root.after(30000, startloop)
else:
root.flag = True
return
startloop()
else:
t_btn.config(text='Start')
root.logger.error("Loop stopped")
root.flag = False


class TextHandler(logging.Handler):

def __init__(self, text):
# run the regular Handler __init__
logging.Handler.__init__(self)
# Store a reference to the Text it will log to
self.text = text

def emit(self, record):
msg = self.format(record)

def append():
self.text.configure(state='normal')
self.text.insert(tk.END, msg + '\n')
self.text.configure(state='disabled')
# Autoscroll to the bottom
self.text.yview(tk.END)

# This is necessary because we can't modify the Text from other threads
self.text.after(0, append)

def create(self):
# Create textLogger
topframe = tk.Frame(root)
topframe.pack(side=tk.TOP)

st = tkst.ScrolledText(topframe, bg="#00A09E", fg="white", state='disabled')
st.configure(font='TkFixedFont')

st.pack()

self.text_handler = TextHandler(st)

# Add the handler to logger
root.logger = logging.getLogger()
root.logger.addHandler(self.text_handler)

def stop(self):
root.flag = False

def start(self):
if root.flag:
root.logger.error("error")
root.after(1000, self.start)
else:
root.logger.error("Loop stopped")
root.flag = True
return

def loop(self):
self.start()


class HomePage(tk.Frame):

def __init__(self, parent):
tk.Frame.__init__(self, parent)

container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)

# NEW added a flag for the Settings window
root.settings = False
self.menubar = tk.Menu(container)

# Create taskbar/menu
file = tk.Menu(self.menubar)
file.add_command(label="Run", command=lambda: test1())
file.add_command(label="Stop", command=lambda: test1())
file.add_separator()

# NEW now calling a method from Settings instead of Settings itself
file.add_command(label="Settings", command=lambda: Settings().open())
file.add_separator()
file.add_command(label="Quit", command=quit)
self.menubar.add_cascade(label="File", menu=file)

self.master.config(menu=self.menubar)

#logger and main loop
th = TextHandler("none")
th.create()
root.flag = True
root.logger.error("Welcome to ShiptScraper!")

bottomframe = tk.Frame(self)
bottomframe.pack(side=tk.BOTTOM)

topframe = tk.Frame(self)
topframe.pack(side=tk.TOP)

self.t_btn = tk.Button(text="Start", highlightbackground="#56B426", command=lambda: toggle(self))
self.t_btn.pack(pady=5)

self.exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
self.exitButton.pack()
root.setting = False


class Settings(tk.Toplevel):

def __init__(self, master=None):
tk.Toplevel.__init__(self, master)

# NEW 'open' method which is being called. This checks the root.setting flag added in the HomePage class
def open(self):
#NEW if root setting is false, continue creation of of Settings window
if not root.setting:
self.wm_title("Settings")
# added grab_set()
Settings.grab_set(self)

#NEW edited the exitButton command, see close function below
exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=lambda: close())
exitButton.pack()
root.setting = True
#NEW if the root.settings flag is TRUE this cancels window creation
else:
self.destroy()

#NEW when close() is called it resets the root.setting flag to false, then destroys the window
def close():
root.setting = False
self.destroy()


class Help(tk.Toplevel):

def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.wm_title("Help")

exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
exitButton.pack()


if __name__ == "__main__":
root = tk.Tk()
root.configure(background="#56B426")
root.wm_title("ShiptScraper")
app = HomePage(root)
app.mainloop()

这感觉像是一个完整而彻底的 hack,我觉得看着它很脏,甚至因为制造这种可憎的东西而更脏......但它有效,至少现在是这样

编辑 3:

在 Jacob 的回答中添加了关闭窗口的协议(protocol)。忘了解释这一点。这是我将分享的最后一个版本,除非我想出更好的方法。

import tkinter as tk
import tkinter.scrolledtext as tkst
from tkinter import ttk
import logging
import time



def popupmsg(msg):
popup = tk.Toplevel()
popup.wm_title("!")
label = ttk.Label(popup, text=msg)
label.pack(side="top", fill="x", pady=10)
b1 = ttk.Button(popup, text="Okay", command=popup.destroy)
b1.pack()
popup.mainloop()


def test1():
root.logger.error("Test")


def toggle(self):
t_btn = self.t_btn
if t_btn.config('text')[-1] == 'Start':
t_btn.config(text='Stop')

def startloop():
if root.flag:
now = time.strftime("%c")
root.logger.error(now)
root.after(30000, startloop)
else:
root.flag = True
return
startloop()
else:
t_btn.config(text='Start')
root.logger.error("Loop stopped")
root.flag = False


class TextHandler(logging.Handler):

def __init__(self, text):
# run the regular Handler __init__
logging.Handler.__init__(self)
# Store a reference to the Text it will log to
self.text = text

def emit(self, record):
msg = self.format(record)

def append():
self.text.configure(state='normal')
self.text.insert(tk.END, msg + '\n')
self.text.configure(state='disabled')
# Autoscroll to the bottom
self.text.yview(tk.END)

# This is necessary because we can't modify the Text from other threads
self.text.after(0, append)

def create(self):
# Create textLogger
topframe = tk.Frame(root)
topframe.pack(side=tk.TOP)

st = tkst.ScrolledText(topframe, bg="#00A09E", fg="white", state='disabled')
st.configure(font='TkFixedFont')

st.pack()

self.text_handler = TextHandler(st)

# Add the handler to logger
root.logger = logging.getLogger()
root.logger.addHandler(self.text_handler)

def stop(self):
root.flag = False

def start(self):
if root.flag:
root.logger.error("error")
root.after(1000, self.start)
else:
root.logger.error("Loop stopped")
root.flag = True
return

def loop(self):
self.start()


class HomePage(tk.Frame):

def __init__(self, parent):
tk.Frame.__init__(self, parent)

container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)

# NEW added a flag for the Settings window
root.setting = True
self.menubar = tk.Menu(container)

# Create taskbar/menu
file = tk.Menu(self.menubar)
file.add_command(label="Run", command=lambda: test1())
file.add_command(label="Stop", command=lambda: test1())
file.add_separator()

# NEW now calling a method from Settings instead of Settings itself
file.add_command(label="Settings", command=lambda: Settings().open())
file.add_separator()
file.add_command(label="Quit", command=quit)
self.menubar.add_cascade(label="File", menu=file)

self.master.config(menu=self.menubar)

#logger and main loop
th = TextHandler("none")
th.create()
root.flag = True
root.logger.error("Welcome to ShiptScraper!")

bottomframe = tk.Frame(self)
bottomframe.pack(side=tk.BOTTOM)

topframe = tk.Frame(self)
topframe.pack(side=tk.TOP)

self.t_btn = tk.Button(text="Start", highlightbackground="#56B426", command=lambda: toggle(self))
self.t_btn.pack(pady=5)

self.exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
self.exitButton.pack()


class Settings(tk.Toplevel):

def __init__(self, master=None):
tk.Toplevel.__init__(self, master)

# NEW 'open' method which is being called. This checks the root.setting flag added in the HomePage class
def open(self):
#NEW when close() is called it resets the root.setting flag to false, then destroys the window
def close_TopLevel():
root.setting = True
self.destroy()

#NEW if root setting is false, continue creation of of Settings window
if root.setting:
self.wm_title("Settings")
#NEW adjust window close protocol and change root.setting to FALSE
self.protocol('WM_DELETE_WINDOW', close_TopLevel)
root.setting = False

#NEW edited the exitButton command, see close function below
exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=lambda: close_TopLevel())
exitButton.pack()

#NEW if the root.settings flag is TRUE this cancels window creation
else:
print('shit')
self.destroy()



class Help(tk.Toplevel):

def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.wm_title("Help")

exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
exitButton.pack()


if __name__ == "__main__":
root = tk.Tk()
root.configure(background="#56B426")
root.wm_title("ShiptScraper")
app = HomePage(root)
app.mainloop()

最佳答案

tkintergrab_set() 正是为此而生。

将下面的代码部分改为:

class Settings(tk.Toplevel):

def __init__(self, master=None):
tk.Toplevel.__init__(self, master)
self.wm_title("Settings")
# added grab_set()
self.grab_set()
#
print(Settings.state(self))

exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
exitButton.pack()

现在,当您打开设置窗口时,如果设置窗口存在,主窗口将不会对按钮点击使用react。

另见 here .


编辑

诡计与欺骗

由于在 Tkinter/OSX 中似乎存在一个关于使用 grab_set() 的错误,它在 Linux (Ubuntu 16.04) 上运行良好,这里有一些诡计和欺骗。

我稍微编辑了你的代码。为简化示例,我将 Toplevel 窗口添加到 HomePage 类。我标记了更改 ##

概念:

  • 向您的类添加一个变量,代表设置窗口存在(或不存在)的事实:

    self.check = False
  • 如果调用“设置”窗口,值会发生变化:

    self.check = True
  • 调用设置窗口的功能现在是被动的。不会出现其他设置窗口:

     def call_settings(self):
    if self.check == False:
    self.settings_window()
  • 我们向“设置”窗口添加协议(protocol),以便在窗口停止存在时运行命令:

    self.settingswin.protocol('WM_DELETE_WINDOW', self.close_Toplevel)
  • 然后调用的函数将重置self.check:

    def close_Toplevel(self):
    self.check = False
    self.settingswin.destroy()

    无论“设置”窗口如何关闭,这都会起作用。

编辑后的代码:

import tkinter as tk
import tkinter.scrolledtext as tkst
from tkinter import ttk
import logging
import time

def popupmsg(msg):
popup = tk.Toplevel()
popup.wm_title("!")
label = ttk.Label(popup, text=msg)
label.pack(side="top", fill="x", pady=10)
b1 = ttk.Button(popup, text="Okay", command=popup.destroy)
b1.pack()
popup.mainloop()

def test1():
root.logger.error("Test")

def toggle(self):
t_btn = self.t_btn
if t_btn.config('text')[-1] == 'Start':
t_btn.config(text='Stop')

def startloop():
if root.flag:
now = time.strftime("%c")
root.logger.error(now)
root.after(30000, startloop)
else:
root.flag = True
return
startloop()
else:
t_btn.config(text='Start')
root.logger.error("Loop stopped")
root.flag = False


class TextHandler(logging.Handler):

def __init__(self, text):
# run the regular Handler __init__
logging.Handler.__init__(self)
# Store a reference to the Text it will log to
self.text = text

def emit(self, record):
msg = self.format(record)

def append():
self.text.configure(state='normal')
self.text.insert(tk.END, msg + '\n')
self.text.configure(state='disabled')
# Autoscroll to the bottom
self.text.yview(tk.END)

# This is necessary because we can't modify the Text from other threads
self.text.after(0, append)

def create(self):
# Create textLogger
topframe = tk.Frame(root)
topframe.pack(side=tk.TOP)

st = tkst.ScrolledText(topframe, bg="#00A09E", fg="white", state='disabled')
st.configure(font='TkFixedFont')

st.pack()

self.text_handler = TextHandler(st)

# Add the handler to logger
root.logger = logging.getLogger()
root.logger.addHandler(self.text_handler)

def stop(self):
root.flag = False

def start(self):
if root.flag:
root.logger.error("error")
root.after(1000, self.start)
else:
root.logger.error("Loop stopped")
root.flag = True
return

def loop(self):
self.start()

class HomePage(tk.Frame):

def __init__(self, parent):
tk.Frame.__init__(self, parent)

container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)

self.menubar = tk.Menu(container)
self.check = False ### new

# Create taskbar/menu
file = tk.Menu(self.menubar)
file.add_command(label="Run", command=lambda: test1())
file.add_command(label="Stop", command=lambda: test1())
file.add_separator()
file.add_command(label="Settings", command=self.call_settings) #### new, changed command to run the function
file.add_separator()
file.add_command(label="Quit", command=quit)
self.menubar.add_cascade(label="File", menu=file)

self.master.config(menu=self.menubar)

#logger and main loop
th = TextHandler("none")
th.create()
root.flag = True
root.logger.error("Welcome to ShiptScraper!")

bottomframe = tk.Frame(self)
bottomframe.pack(side=tk.BOTTOM)

topframe = tk.Frame(self)
topframe.pack(side=tk.TOP)

self.t_btn = tk.Button(text="Start", highlightbackground="#56B426", command=lambda: toggle(self))
self.t_btn.pack(pady=5)
self.exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
self.exitButton.pack()
root.setting = False

########## changed
def call_settings(self):
if self.check == False:
self.settings_window()
##########

def settings_window(self):
self.check = True
self.settingswin = tk.Toplevel()
self.settingswin.wm_title("Settings")
self.settingswin.protocol('WM_DELETE_WINDOW', self.close_Toplevel) ##### new
exitButton = tk.Button(self.settingswin, text="Exit", highlightbackground="#56B426", command=self.close_Toplevel)
exitButton.pack()

def close_Toplevel(self):
# New, this runs when the Toplevel window closes, either by button or else
self.check = False
self.settingswin.destroy()

class Help(tk.Toplevel):

def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.wm_title("Help")
exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
exitButton.pack()

if __name__ == "__main__":
root = tk.Tk()
root.configure(background="#56B426")
root.wm_title("ShiptScraper")
app = HomePage(root)
app.mainloop()

注意事项

一旦我们触发设置窗口的存在,我们当然可以做更多的事情,例如禁用主窗口上的所有按钮。通过这种方式,我们创建了自己的 grab_set() 版本,但更加灵活。

关于python - Tkinter:只允许一个 Toplevel 窗口实例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39689046/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com