【问题标题】:Tkinter GUI Not responding when running tasks运行任务时 Tkinter GUI 没有响应
【发布时间】:2021-11-12 10:42:19
【问题描述】:

我正在尝试制作防病毒应用程序,但遇到了一些问题。在我按下立即扫描按钮后,GUI 变得不可点击且无响应,但程序仍在我的 IDE 中运行。我正在考虑限制每秒读取的文件,这可能吗?或者这个问题还有其他解决方案吗?

这是我的编码:

def md5(self,fname):
    hash_md5 = hashlib.md5()
    try:
        with open(fname, "rb") as f:
            for chunk in iter(lambda: f.read(2 ** 20), b""):
                hash_md5.update(chunk)
    except Exception:
        pass
    return hash_md5.hexdigest()

def get_all_abs_paths(self,rootdir):
    self.progressBar['value'] = self.progressBar['value'] + 5
    viruslist = open('C:\FYP\SecuCOM2022\compile.txt','rt')
    virusinside = [l.rstrip() for l in viruslist]
    paths = list()
    virus="detected"
    novirus="clear"
    for dirpath,_,filenames in os.walk(rootdir):
        for f in filenames:
            paths.append(os.path.abspath(os.path.join(dirpath, f)))
def getfile(self):
    
    file2=('C:/Windows/System32')
    file3=('C:/Program Files')
    self.status.set("Scanning...")
    self.progressBar['value'] = self.progressBar['value'] + 5   
    self.get_all_abs_paths(file3)
    self.progressBar['value'] = self.progressBar['value'] + 45
    self.get_all_abs_paths(file2)
    self.status.set("Finished Scan")
    self.progressBar['value'] = self.progressBar['value'] + 50

【问题讨论】:

    标签: python tkinter md5


    【解决方案1】:

    你需要创建一个线程来运行扫描功能,见https://pypi.org/project/pythread/

    现在发生的情况是 Tkinter gui 和 scan 函数在应用程序的同一个唯一线程中运行,因此当 scan 函数运行时 gui 不是,这就是它冻结的原因。

    通过创建线程,您将同时并行运行 gui 和扫描功能。

    【讨论】:

      【解决方案2】:

      Tkinter GUI 有一个mainloop,调用任何函数都会导致它暂停,直到函数退出。 您可以在单独的线程上运行该函数,以允许 GUI 的其余部分在任务完成时工作。

      【讨论】:

        【解决方案3】:

        tkinter 等 GUI 工具包是事件驱动的。为了正常工作, mainloop 必须能够持续处理键盘和鼠标事件。 当它不处理事件时,它会启动预定的空闲任务

        因此它们的工作方式与正常运行的 Python 脚本完全不同 从上到下。

        tkinter 程序mainloop 中运行。所以只有三个 在开始主循环之前你要做的事情。

        1. 创建一个带有一些小部件的窗口。
        2. 创建保存程序状态的对象(变量)。
        3. 将可以从mainloop 运行的函数定义为回调空闲任务

        调用回调以响应激活控件(例如单击 一个按钮)。 系统在指定的毫秒数后启动空闲任务 当系统不忙于处理事件时。您可以安排空闲任务 使用Tk.after() 方法。

        基本上,回调和空闲任务是你的程序

        为了保持 GUI 响应,回调和空闲任务不应花费太长时间;比如说 50 毫秒。

        因此,在单个回调中运行完整的防病毒扫描确实会使 GUI 无响应。

        基本上,有三种可能的解决方案。

        1. 将扫描过程分成小块。第一部分在回调中执行,其余部分在空闲任务中完成。这可能是最简单的解决方案,因为它可以让您在扫描时轻松更新 GUI。
        2. 使用multiprocessing 启动一个单独的程序来进行扫描。设置Pipe 以启用 GUI 和扫描程序之间的通信。在 GUI 中使用空闲任务从管道中读取消息并相应地更新 GUI。
        3. 使用threading 启动一个单独的线程来进行扫描。关于 tkinter 实际上可以安全地从多个线程调用存在一些混淆;请参阅下面的示例。

        哪一个最适合你我很难说。

        下面我以tkinter脚本的形式给出1和3的例子来解锁ms-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__ = "2020.04.20"
        
        
        def create_widgets(root):
            """Create the window and its widgets.
        
            Arguments:
                root: the root window.
        
            Returns:
                A SimpleNamespace of 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)
            # A SimpleNamespace is used to save widgets that need to be accessed later.
            w = SimpleNamespace()
            # First row
            ttk.Label(root, text='(1)').grid(row=0, column=0, sticky='ew')
            fb = ttk.Button(root, text="Select file", command=do_file)
            fb.grid(row=0, column=1, columnspan=2, sticky="w")
            w.fb = fb
            fn = ttk.Label(root)
            fn.grid(row=0, column=3, columnspan=2, sticky="ew")
            w.fn = fn
            # Second row
            ttk.Label(root, text='(2)').grid(row=1, column=0, sticky='ew')
            backup = tk.IntVar()
            backup.set(0)
            w.backup = backup
            ttk.Checkbutton(root, text='backup', variable=backup,
                            command=on_backup).grid(row=1, column=1, sticky='ew')
            suffixlabel = ttk.Label(root, text='suffix:')
            suffixlabel['state'] = 'disabled'
            suffixlabel.grid(row=1, column=2, sticky='ew')
            w.suffixlabel = suffixlabel
            suffix = tk.StringVar()
            suffix.set('-orig')
            w.suffix = suffix
            se = ttk.Entry(root, justify='left', textvariable=suffix)
            se.grid(row=1, column=3, columnspan=1, sticky='w')
            se['state'] = 'disabled'
            w.suffixentry = se
            # Third row
            ttk.Label(root, text='(3)').grid(row=2, column=0, sticky='ew')
            gobtn = ttk.Button(root, text="Go!", command=do_start)
            gobtn['state'] = 'disabled'
            gobtn.grid(row=2, column=1, sticky='ew')
            w.gobtn = gobtn
            # 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")
            status = tk.Listbox(root, width=40, yscrollcommand=sb.set)
            status.grid(row=4, rowspan=5, column=1, columnspan=3, sticky="nsew")
            w.status = status
            sb.grid(row=4, rowspan=5, column=5, sticky="ns")
            sb.config(command=status.yview)
            # Ninth row
            ttk.Button(root, text="Quit", command=do_exit).grid(row=9, column=1, sticky='ew')
            # Return the widgets that need to be accessed.
            return w
        
        
        def create_state():
            """Create and initialize the global state."""
            state = SimpleNamespace()
            state.interval = 10
            state.path = ''
            state.inzf, state.outzf = None, None
            state.infos = None
            state.currinfo = None
            state.worksheets_unlocked = 0
            state.workbook_unlocked = False
            state.directory = None
            state.remove = None
            return state
        
        
        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
            # Widgets is a namespace of widgets that needs to be accessed by the callbacks.
            # State is a namespace of the global state.
            widgets = create_widgets(root)
            state = create_state()
            root.mainloop()
        

        然后是使用线程的版本:

        """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__ = "2020.04.27"
        
        
        def create_widgets(root):
            """Create the window and its widgets.
        
            Arguments:
                root: the root window.
        
            Returns:
                A SimpleNamespace of 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)
            # A SimpleNamespace is used to save widgets that need to be accessed later.
            w = SimpleNamespace()
            # First row
            ttk.Label(root, text='(1)').grid(row=0, column=0, sticky='ew')
            fb = ttk.Button(root, text="Select file", command=do_file)
            fb.grid(row=0, column=1, columnspan=2, sticky="w")
            w.fb = fb
            fn = ttk.Label(root)
            fn.grid(row=0, column=3, columnspan=2, sticky="ew")
            w.fn = fn
            # Second row
            ttk.Label(root, text='(2)').grid(row=1, column=0, sticky='ew')
            backup = tk.IntVar()
            backup.set(0)
            w.backup = backup
            ttk.Checkbutton(root, text='backup', variable=backup,
                            command=on_backup).grid(row=1, column=1, sticky='ew')
            suffixlabel = ttk.Label(root, text='suffix:')
            suffixlabel['state'] = 'disabled'
            suffixlabel.grid(row=1, column=2, sticky='ew')
            w.suffixlabel = suffixlabel
            suffix = tk.StringVar()
            suffix.set('-orig')
            w.suffix = suffix
            se = ttk.Entry(root, justify='left', textvariable=suffix)
            se.grid(row=1, column=3, columnspan=1, sticky='w')
            se['state'] = 'disabled'
            w.suffixentry = se
            # Third row
            ttk.Label(root, text='(3)').grid(row=2, column=0, sticky='ew')
            gobtn = ttk.Button(root, text="Go!", command=do_start)
            gobtn['state'] = 'disabled'
            gobtn.grid(row=2, column=1, sticky='ew')
            w.gobtn = gobtn
            # 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")
            status = tk.Listbox(root, width=60, yscrollcommand=sb.set)
            status.grid(row=4, rowspan=5, column=1, columnspan=3, sticky="nsew")
            w.status = status
            sb.grid(row=4, rowspan=5, column=5, sticky="ns")
            sb.config(command=status.yview)
            # Ninth row
            ttk.Button(root, text="Quit", command=do_exit).grid(row=9, column=1, sticky='ew')
            # Return the widgets that need to be accessed.
            return w
        
        
        def create_state():
            """Create and initialize the global state."""
            st = SimpleNamespace()
            st.directory = None
            return st
        
        
        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
            # Widgets is a namespace of widgets that needs to be accessed by the callbacks.
            # State is a namespace of the global state.
            widgets = create_widgets(root)
            state = create_state()
            root.mainloop()
        

        【讨论】:

          【解决方案4】:

          您可以使用 threading.Thread 来运行函数。 threading.Thread 就像是新开了一个cpu线程来运行函数

          from threading import Thread
          # Code....
          
          Thread(target=function()).start
          

          startstart() 之间存在一些差异。 start 开始执行函数如果某个按钮或某个命令告诉线程运行。但是start() 在代码运行时执行。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2021-10-07
            • 1970-01-01
            • 2015-01-26
            • 2021-03-06
            • 2022-01-21
            • 2010-09-14
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多