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

时间:2016-09-25 16:14:45

标签: python tkinter

我有一个带有多个窗口的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()

这感觉就像一个完整而彻底的黑客,我觉得看起来很脏,甚至更脏,因为造成这种憎恶...但它起作用,至少现在

编辑3:

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

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

1 个答案:

答案 0 :(得分:1)

tkinter&#39; s grab_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()

现在,当您打开设置窗口时,在设置窗口存在时,主窗口不会对按钮单击做出反应。

另见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()
    
  • 我们在“设置”窗口中添加协议,以便在窗口停止存在时运行命令:

    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()版本,但更加灵活。