简体   繁体   English

在 Tkinter 中执行函数期间程序顶层冻结

[英]Program Toplevel freezing during the execution of a function in Tkinter

I have a tkinter app that is structured like that:我有一个结构如下的 tkinter 应用程序:

import tkinter as tk
import tkinter.ttk as ttk


class TopLevelWindow(tk.Toplevel):
    def __init__(self, root, *args, **kargs):
        super().__init__(root, *args, **kargs)
        self.root = root
        self.button= ttk.Button(self, text="Exportar excel", command=self.go)
        self.button.pack()
        self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
        self.prog_bar.pack()

    def go(self):
        global dic_Data
        # Create word file
        value_bar = 100/len(dic_Data)
        for k, v in dic_Data.items():
            # Calculate some stuff
            # Create Pil images and add them to the word file
            # Create Matplotlib images and add them to the word file
            self.prog_bar["value"] += value_bar
            self.root.update_idletasks()
        #Save the word file
        self.quit()
        self.destroy()


class App():
    def __init__(self, root, *params):
        self.root = root
        # Code
        # dic_Data gets populated

    def open_window(self):
        popup = TopLevelWindow(self.root)
        popup.mainloop()


dic_Data = {}

root = tk.Tk()
app = App(root)
root.mainloop()

The problem I'm having is that the function go ends up freezing my Toplevel window.我遇到的问题是函数go最终冻结了我的顶层窗口。

What is weird is that is starts doing okey for the first items of the for loop, then it freezes but the functions keeps running fine (no visual updates on the bar), until the for loop finishes and after that the windows closes as its suposed to and everything unfreezes again.奇怪的是,它开始为 for 循环的第一个项目做 okey,然后它冻结但函数继续运行良好(栏上没有视觉更新),直到 for 循环完成,然后窗口按照它的假设关闭一切都再次解冻。

Why is that happening and could it be fixed?为什么会发生这种情况,可以解决吗?

EDITED: One answer suggests to use the root.after function to avoid freezing but im having problems implementing it.已编辑:一个答案建议使用 root.after 函数来避免冻结,但我在实现它时遇到了问题。 This is what i have:这就是我所拥有的:

import tkinter as tk
import tkinter.ttk as ttk


class TopLevelWindow(tk.Toplevel):
    def __init__(self, root, *args, **kargs):
        super().__init__(root, *args, **kargs)
        self.root = root
        self.button = ttk.Button(self, text="Exportar excel", command=self.go)
        self.button.pack()
        self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
        self.prog_bar.pack()

    def heavy_function(self, k, v):
        # Calculate some stuff
        # Create Pil images and add the to the word file
        # Create Matplotlib images and add the to the word file
        # Add changes to word file
        self.prog_bar["value"] += value_bar

    def save_word(self):
        #Save the word file
        self.quit()
        self.destroy()

    def go(self):
        global dic_Data
        # Create word file
        value_bar = 100/len(dic_Data)
        for k, v in dic_Data.items():
            self.root.after(10, lambda: self.heavy_function(k, v))
        self.root.after(10, self.save_word)


class App():
    def __init__(self, root, *params):
        self.root = root
        # Code
        # dic_Data gets populated

    def open_window(self):
        popup = TopLevelWindow(self.root)
        popup.mainloop()


dic_Data = {}

root = tk.Tk()
app = App(root)
root.mainloop()

tl;dr What happens is that self.go simply takes too long and stops the processing of events. tl; dr发生的情况是self.go花费了太长时间并停止了事件的处理。

Callbacks like self.go are executed in the mainloop whenever you activate a control.每当您激活控件时,就会mainloop中执行self.go之类的回调。 As long as the callback is is running, event processing is interrupted.只要回调正在运行,事件处理就会中断。 So if said callback takes more than say 50 ms, you will notice it.因此,如果所述回调花费的时间超过 50 毫秒,您会注意到它。

There are basically three ways to do long-running jobs in programs that use event-driven toolkits like tkinter.在使用事件驱动工具包(如 tkinter)的程序中,基本上有三种方法可以执行长时间运行的作业。

  1. Chop up the long running jobs in small pieces that are executed using the after method.将长时间运行的作业切成小块,使用after方法执行。 This makes it easy to update a progress bar.这使得更新进度条变得容易。 An example from my own code is shown below.下面显示了我自己的代码中的一个示例。

  2. Execute the long-running job in a separate process with multiprocessing .使用multiprocessing在单独的进程中执行长时间运行的作业。 This will definitely not block the GUI.这绝对不会阻塞 GUI。 But you will need to use communication primitives to tell the GUI that an action is finished, and the GUI has to check those regularly using the after method.但是您将需要使用通信原语来告诉 GUI 一个动作已经完成,并且 GUI 必须使用after方法定期检查这些原语。

  3. Execute the long-running job in a separate thread.在单独的线程中执行长时间运行的作业。 An additional complication is that in CPython only one thread at a time can be executing Python bytecode.另一个复杂情况是,在 CPython 中,一次只能有一个线程执行 Python 字节码。 So this is not guaranteed to be free from blocking the GUI.所以这不能保证不会阻塞 GUI。 But in general, Python 3 tries to switch threads pretty often.但总的来说,Python 3 会经常尝试切换线程。 You will need a tkinter that is built with threading enabled.您将需要一个启用线程的tkinter An example is shown below.一个例子如下所示。

Example of (1): unlocking excel files using small steps (一)示例:使用小步骤解锁excel文件

"""Remove passwords from modern excel 2007+ files (xlsx, xlsm)."""

from types import SimpleNamespace
import os
import re
import shutil
import stat
import sys
import zipfile

from tkinter import filedialog
from tkinter import ttk
from tkinter.font import nametofont
import tkinter as tk

__version__ = "2022.01.28"
widgets = SimpleNamespace()
state = SimpleNamespace()


def create_widgets(root, w):
    """Create the window and its widgets.
    Arguments:
        root: the root window.
        w: SimpleNamespace to store widgets.
    """
    # Set the font.
    default_font = nametofont("TkDefaultFont")
    default_font.configure(size=12)
    root.option_add("*Font", default_font)
    # General commands and bindings
    root.bind_all('q', do_exit)
    root.wm_title('Unlock excel files v' + __version__)
    root.columnconfigure(3, weight=1)
    root.rowconfigure(5, weight=1)
    # First row
    ttk.Label(root, text='(1)').grid(row=0, column=0, sticky='ew')
    w.fb = ttk.Button(root, text="Select file", command=do_file)
    w.fb.grid(row=0, column=1, columnspan=2, sticky="w")
    w.fn = ttk.Label(root)
    w.fn.grid(row=0, column=3, columnspan=2, sticky="ew")
    # Second row
    ttk.Label(root, text='(2)').grid(row=1, column=0, sticky='ew')
    w.backup = tk.IntVar()
    w.backup.set(0)
    ttk.Checkbutton(root, text='backup', variable=w.backup,
                    command=on_backup).grid(row=1, column=1, sticky='ew')
    w.suffixlabel = ttk.Label(root, text='suffix:', state=tk.DISABLED)
    w.suffixlabel.grid(row=1, column=2, sticky='ew')
    w.suffix = tk.StringVar()
    w.suffix.set('-orig')
    se = ttk.Entry(root, justify='left', textvariable=w.suffix, state=tk.DISABLED)
    se.grid(row=1, column=3, columnspan=1, sticky='w')
    w.suffixentry = se
    # Third row
    ttk.Label(root, text='(3)').grid(row=2, column=0, sticky='ew')
    w.gobtn = ttk.Button(root, text="Go!", command=do_start, state=tk.DISABLED)
    w.gobtn.grid(row=2, column=1, sticky='ew')
    # Fourth row
    ttk.Label(root, text='(4)').grid(row=3, column=0, sticky='ew')
    ttk.Label(root, text='Progress:').grid(row=3, column=1, sticky='w')
    # Fifth row
    sb = tk.Scrollbar(root, orient="vertical")
    w.status = tk.Listbox(root, width=60, yscrollcommand=sb.set)
    w.status.grid(row=4, rowspan=5, column=1, columnspan=3, sticky="nsew")
    sb.grid(row=4, rowspan=5, column=5, sticky="ns")
    sb.config(command=w.status.yview)
    # Ninth row
    ttk.Button(root, text="Quit", command=do_exit).grid(row=9, column=1, sticky='ew')


def initialize_state(s):
    """
    Initialize the global state.
    Arguments:
        s: SimpleNamespace to store application state.
    """
    s.interval = 10
    s.path = ''
    s.inzf, s.outzf = None, None
    s.infos = None
    s.currinfo = None
    s.worksheets_unlocked = 0
    s.workbook_unlocked = False
    s.directory = None
    s.remove = None


def statusmsg(text):
    """Append a message to the status listbox, and make sure it is visible."""
    widgets.status.insert(tk.END, text)
    widgets.status.see(tk.END)


# Step functions to call in the after() method.
def step_open_zipfiles():
    path = widgets.fn['text']
    state.path = path
    statusmsg(f'Opening “{path}”...')
    first, last = path.rsplit('.', maxsplit=1)
    if widgets.backup.get():
        backupname = first + widgets.suffix.get() + '.' + last
    else:
        backupname = first + '-orig' + '.' + last
        state.remove = backupname
    shutil.move(path, backupname)
    state.inzf = zipfile.ZipFile(backupname, mode="r")
    state.outzf = zipfile.ZipFile(
        path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=1
    )
    root.after(state.interval, step_discover_internal_files)


def step_discover_internal_files():
    statusmsg(f'Reading “{state.path}”...')
    state.infos = [name for name in state.inzf.infolist()]
    state.currinfo = 0
    statusmsg(f'“{state.path}” contains {len(state.infos)} internal files.')
    root.after(state.interval, step_filter_internal_file)


def step_filter_internal_file():
    current = state.infos[state.currinfo]
    stat = f'Processing “{current.filename}” ({state.currinfo+1}/{len(state.infos)})...'
    statusmsg(stat)
    # Doing the actual work
    regex = None
    data = state.inzf.read(current)
    if b'sheetProtect' in data:
        regex = r'<sheetProtect.*?/>'
        statusmsg(f'Worksheet "{current.filename}" is protected.')
    elif b'workbookProtect' in data:
        regex = r'<workbookProtect.*?/>'
        statusmsg('The workbook is protected')
    else:
        state.outzf.writestr(current, data)
    if regex:
        text = data.decode('utf-8')
        newtext = re.sub(regex, '', text)
        if len(newtext) != len(text):
            state.outzf.writestr(current, newtext)
            state.worksheets_unlocked += 1
            statusmsg(f'Removed password from "{current.filename}".')
    # Next iteration or next step.
    state.currinfo += 1
    if state.currinfo >= len(state.infos):
        statusmsg('All internal files processed.')
        state.currinfo = None
        root.after(state.interval, step_close_zipfiles)
    else:
        root.after(state.interval, step_filter_internal_file)


def step_close_zipfiles():
    statusmsg(f'Writing “{state.path}”...')
    state.inzf.close()
    state.outzf.close()
    state.inzf, state.outzf = None, None
    root.after(state.interval, step_finished)


def step_finished():
    if state.remove:
        os.chmod(state.remove, stat.S_IWRITE)
        os.remove(state.remove)
        state.remove = None
    else:
        statusmsg('Removing temporary file')
    statusmsg(f'Unlocked {state.worksheets_unlocked} worksheets.')
    statusmsg('Finished!')
    widgets.gobtn['state'] = 'disabled'
    widgets.fn['text'] = ''
    state.path = ''


# Widget callbacks
def do_file():
    """Callback to open a file"""
    if not state.directory:
        state.directory = ''
        available = [os.environ[k] for k in ('HOME', 'HOMEDRIVE') if k in os.environ]
        if available:
            state.directory = available[0]
    fn = filedialog.askopenfilename(
        title='Excel file to open',
        parent=root,
        defaultextension='.xlsx',
        filetypes=(
            ('excel files', '*.xls*'), ('all files', '*.*')
        ),
    )
    if not fn:
        return
    state.directory = os.path.dirname(fn)
    state.worksheets_unlocked = 0
    state.workbook_unlocked = False
    state.path = fn
    widgets.fn['text'] = fn
    widgets.gobtn['state'] = 'enabled'
    widgets.status.delete(0, tk.END)


def on_backup():
    if widgets.backup.get() == 1:
        widgets.suffixlabel['state'] = 'enabled'
        widgets.suffixentry['state'] = 'enabled'
    else:
        widgets.suffixlabel['state'] = 'disabled'
        widgets.suffixentry['state'] = 'disabled'


def do_start():
    root.after(state.interval, step_open_zipfiles)


def do_exit(arg=None):
    """
    Callback to handle quitting.
    """
    root.destroy()


if __name__ == '__main__':
    # Detach from the command line on UNIX systems.
    if os.name == 'posix':
        if os.fork():
            sys.exit()    # Create the GUI window.
    root = tk.Tk(None)
    # Use a dialog window so that it floats even when using a tiling window
    # manager.
    root.attributes('-type', 'dialog')
    # Don't show hidden files in the file dialog
    # https://stackoverflow.com/questions/53220711/how-to-avoid-hidden-files-in-file-picker-using-tkinter-filedialog-askopenfilenam
    try:
        # call a dummy dialog with an impossible option to initialize the file
        # dialog without really getting a dialog window; this will throw a
        # TclError, so we need a try...except :
        try:
            root.tk.call('tk_getOpenFile', '-foobarbaz')
        except tk.TclError:
            pass
        # now set the magic variables accordingly
        root.tk.call('set', '::tk::dialog::file::showHiddenBtn', '1')
        root.tk.call('set', '::tk::dialog::file::showHiddenVar', '0')
    except Exception:
        pass
    create_widgets(root, widgets)
    initialize_state(state)
    root.mainloop()

Example of (3) unlocking excel files using a thread (3)使用线程解锁excel文件示例

"""Remove passwords from modern excel 2007+ files (xlsx, xlsm).
This is a multithreaded version of unlock-excel.pyw.  All the work that was
there done in steps in the mainloop is now done in a single additional thread.
There is some confusion whether tkinter is thread-safe.  That is, if one can
call tkinter functions and methods from any but the main thread.  The
documentation for Python 3 says “yes”.  Comments in the C source code for
tkinter say “its complicated” depending on how tcl is built.  *Many* online
sources say “no”, but that could just be an echo chamber effect.
The author has tested this code on FreeBSD 12.1-STABLE amd64 using CPython
3.7.7 combined with a tcl built with threading enabled.  There at least it
seems to work without problems.
"""

from types import SimpleNamespace
import os
import re
import shutil
import stat
import sys
import threading
import zipfile

from tkinter import filedialog
from tkinter import ttk
from tkinter.font import nametofont
import tkinter as tk

__version__ = "2022.01.28"
widgets = SimpleNamespace()
state = SimpleNamespace()


def create_widgets(root, w):
    """Create the window and its widgets.
    Arguments:
        root: the root window.
        w: SimpleNamespace to store widgets.
    """
    # Set the font.
    default_font = nametofont("TkDefaultFont")
    default_font.configure(size=12)
    root.option_add("*Font", default_font)
    # General commands and bindings
    root.bind_all('q', do_exit)
    root.wm_title('Unlock excel files v' + __version__)
    root.columnconfigure(3, weight=1)
    root.rowconfigure(5, weight=1)
    # First row
    ttk.Label(root, text='(1)').grid(row=0, column=0, sticky='ew')
    w.fb = ttk.Button(root, text="Select file", command=do_file)
    w.fb.grid(row=0, column=1, columnspan=2, sticky="w")
    w.fn = ttk.Label(root)
    w.fn.grid(row=0, column=3, columnspan=2, sticky="ew")
    # Second row
    ttk.Label(root, text='(2)').grid(row=1, column=0, sticky='ew')
    w.backup = tk.IntVar()
    w.backup.set(0)
    ttk.Checkbutton(root, text='backup', variable=w.backup,
                    command=on_backup).grid(row=1, column=1, sticky='ew')
    w.suffixlabel = ttk.Label(root, text='suffix:', state=tk.DISABLED)
    w.suffixlabel.grid(row=1, column=2, sticky='ew')
    w.suffix = tk.StringVar()
    w.suffix.set('-orig')
    se = ttk.Entry(root, justify='left', textvariable=w.suffix, state=tk.DISABLED)
    se.grid(row=1, column=3, columnspan=1, sticky='w')
    w.suffixentry = se
    # Third row
    ttk.Label(root, text='(3)').grid(row=2, column=0, sticky='ew')
    w.gobtn = ttk.Button(root, text="Go!", command=do_start, state=tk.DISABLED)
    w.gobtn.grid(row=2, column=1, sticky='ew')
    # Fourth row
    ttk.Label(root, text='(4)').grid(row=3, column=0, sticky='ew')
    ttk.Label(root, text='Progress:').grid(row=3, column=1, sticky='w')
    # Fifth row
    sb = tk.Scrollbar(root, orient="vertical")
    w.status = tk.Listbox(root, width=60, yscrollcommand=sb.set)
    w.status.grid(row=4, rowspan=5, column=1, columnspan=3, sticky="nsew")
    sb.grid(row=4, rowspan=5, column=5, sticky="ns")
    sb.config(command=w.status.yview)
    # Ninth row
    ttk.Button(root, text="Quit", command=do_exit).grid(row=9, column=1, sticky='ew')


def initialize_state(s):
    """
    Initialize the global state.
    Arguments:
        s: SimpleNamespace to store application state.
    """
    s.directory = None


def statusmsg(text):
    """Append a message to the status listbox, and make sure it is visible."""
    widgets.status.insert(tk.END, text)
    widgets.status.see(tk.END)


def process_zipfile_thread():
    """Function to process a zip-file. This is to be run in a thread."""
    path = widgets.fn['text']
    statusmsg(f'Opening “{path}”...')
    first, last = path.rsplit('.', maxsplit=1)
    if widgets.backup.get():
        backupname = first + widgets.suffix.get() + '.' + last
        remove = None
    else:
        backupname = first + '-orig' + '.' + last
        remove = backupname
    shutil.move(path, backupname)
    with zipfile.ZipFile(backupname, mode="r") as inzf, \
            zipfile.ZipFile(
                path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=1
            ) as outzf:
        statusmsg(f'Reading “{path}”...')
        infos = [name for name in inzf.infolist()]
        statusmsg(f'“{path}” contains {len(infos)} internal files.')
        worksheets_unlocked = 0
        for idx, current in enumerate(infos, start=1):
            smsg = f'Processing “{current.filename}” ({idx}/{len(infos)})...'
            statusmsg(smsg)
            # Doing the actual work
            regex = None
            data = inzf.read(current)
            if b'sheetProtect' in data:
                regex = r'<sheetProtect.*?/>'
                statusmsg(f'Worksheet "{current.filename}" is protected.')
            elif b'workbookProtect' in data:
                regex = r'<workbookProtect.*?/>'
                statusmsg('The workbook is protected')
            else:
                outzf.writestr(current, data)
            if regex:
                text = data.decode('utf-8')
                newtext = re.sub(regex, '', text)
                if len(newtext) != len(text):
                    outzf.writestr(current, newtext)
                    worksheets_unlocked += 1
                    statusmsg(f'Removed password from "{current.filename}".')
    statusmsg('All internal files processed.')
    statusmsg(f'Writing “{path}”...')
    if remove:
        os.chmod(remove, stat.S_IWRITE)
        os.remove(remove)
    else:
        statusmsg('Removing temporary file')
    statusmsg(f'Unlocked {state.worksheets_unlocked} worksheets.')
    statusmsg('Finished!')
    widgets.gobtn['state'] = 'disabled'
    widgets.fn['text'] = ''


# Widget callbacks
def do_file():
    """Callback to open a file"""
    if not state.directory:
        state.directory = ''
        available = [os.environ[k] for k in ('HOME', 'HOMEDRIVE') if k in os.environ]
        if available:
            state.directory = available[0]
    fn = filedialog.askopenfilename(
        title='Excel file to open',
        parent=root,
        defaultextension='.xlsx',
        filetypes=(('excel files', '*.xls*'), ('all files', '*.*')),
    )
    if not fn:
        return
    state.directory = os.path.dirname(fn)
    state.worksheets_unlocked = 0
    state.workbook_unlocked = False
    widgets.fn['text'] = fn
    widgets.gobtn['state'] = 'enabled'
    widgets.status.delete(0, tk.END)


def on_backup():
    if widgets.backup.get() == 1:
        widgets.suffixlabel['state'] = 'enabled'
        widgets.suffixentry['state'] = 'enabled'
    else:
        widgets.suffixlabel['state'] = 'disabled'
        widgets.suffixentry['state'] = 'disabled'


def do_start():
    worker = threading.Thread(target=process_zipfile_thread)
    worker.start()


def do_exit(arg=None):
    """
    Callback to handle quitting.
    """
    root.destroy()


if __name__ == '__main__':
    # Detach from the command line on UNIX systems.
    if os.name == 'posix':
        if os.fork():
            sys.exit()
    # Create the GUI window.
    root = tk.Tk(None)
    # Use a dialog window so that it floats even when using a tiling window manager.
    if os.name == 'posix':
        root.attributes('-type', 'dialog')
    # Don't show hidden files in the file dialog
    # https://stackoverflow.com/questions/53220711/how-to-avoid-hidden-files-in-file-picker-using-tkinter-filedialog-askopenfilenam
    try:
        # call a dummy dialog with an impossible option to initialize the file
        # dialog without really getting a dialog window; this will throw a
        # TclError, so we need a try...except :
        try:
            root.tk.call('tk_getOpenFile', '-foobarbaz')
        except tk.TclError:
            pass
        # now set the magic variables accordingly
        root.tk.call('set', '::tk::dialog::file::showHiddenBtn', '1')
        root.tk.call('set', '::tk::dialog::file::showHiddenVar', '0')
    except Exception:
        pass
    create_widgets(root, widgets)
    initialize_state(state)
    root.mainloop()

I've managed to implement Roland Smith answer option 1. Now it works, hopefuly this makes sense:我已经设法实现了 Roland Smith 答案选项 1。现在它可以工作了,希望这是有道理的:

import tkinter as tk
import tkinter.ttk as ttk


class TopLevelWindow(tk.Toplevel):
    def __init__(self, root, *args, **kargs):
        super().__init__(root, *args, **kargs)
        self.root = root
        self.button = ttk.Button(self, text="Exportar excel", command=self.go)
        self.button.pack()
        self.prog_bar = ttk.Progressbar(self, orient = "horizontal", mode= "determinate")
        self.prog_bar.pack()

    def heavy_function(self):
        try:
            k, v = next(self.order)
        except StopIteration:
            self.after(10, self.save_word)
            return
        # Calculate some stuff
        # Create Pil images and add the to the word file
        # Create Matplotlib images and add the to the word file
        self.prog_bar["value"] += self.value_bar
        self.after(10, self.heavy_function)

    def save_word(self):
        #Save the word file
        self.quit()
        self.destroy()

    def go(self):
        global dic_Data
        # Create word file
        self.value_bar = 100/len(dic_Data)
        self.order = iter(dic_Data.items())
        self.after(10, self.heavy_function)


class App():
    def __init__(self, root, *params):
        self.root = root
        # Code
        # dic_Data gets populated

    def open_window(self):
        popup = TopLevelWindow(self.root)
        popup.mainloop()


dic_Data = {}

root = tk.Tk()
app = App(root)
root.mainloop()

You have named the button with the same name as the function (go).您已将按钮命名为与函数 (go) 同名的名称。 Instead, you have to name it something else and pack it as well as the progress bar:相反,您必须将其命名为其他名称并将其与进度条一起打包:

import time
import tkinter as tk
import tkinter.ttk as ttk


class TopLevelWindow(tk.Toplevel):
    def __init__(self, root, *args, **kargs):
        super().__init__(root, *args, **kargs)
        self.root = root
        self.button = ttk.Button(self, text="Exportar excel", command=self.go)
        self.button.pack()  # packing button
        self.prog_bar = ttk.Progressbar(self, orient="horizontal", mode="determinate")
        self.prog_bar.pack()  # packing progress bar

    def go(self):
        global dic_Data
        # Create word file
        value_bar = 100 / len(dic_Data)
        for k, v in dic_Data.items():
            time.sleep(1)  # to visualize the progressbar
            # Calculate some stuff
            # Create Pil images and add them to the word file
            # Create Matplotlib images and add them to the word file
            self.prog_bar["value"] += value_bar
            self.root.update_idletasks()
        # Save the word file
        self.quit()
        self.destroy()


class App:
    def __init__(self, root, *params):
        self.root = root
        # Code
        # dic_Data gets populated

    def open_window(self):
        popup = TopLevelWindow(self.root)
        popup.mainloop()


# some data to avoid divide by zero
dic_Data = {'a': 'Something', 'B': 'Something else', 'C': 'Nothing', 'D': 'Nothing else'}

root = tk.Tk()
app = App(root)
app.open_window()  # calling the open window function
root.mainloop()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM