I am trying to make an antivirus application but I met some issue. After I press the button Scan Now, the GUI become unclickable and Not Responding but the program still running in my IDE. I am thinking to limit the file read per second, is this possible? Or is there any others solution for this issue?
This is my coding:
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
CodePudding user response:
You need to create a thread to run the scan function, see https://pypi.org/project/pythread/
What happens right now is that the Tkinter gui and the scan function run in the same unique thread of your application, so when scan function is running the gui is not, which is why it freeze.
By creating a thread you will have both the gui and the scan function running at the same time in parallel.
CodePudding user response:
Tkinter GUIs have a mainloop
and calling any function will cause it to pause until the function is exited.
You can run the function on a seperate thread to allow the rest of the GUI to work while a task is being done.
CodePudding user response:
GUI toolkits like tkinter
are event-driven. To work properly, the
mainloop
must continuously be able to process keyboard and mouse events.
When it is not processing events, it will start scheduled idle tasks.
So they work quite differently from normal Python scripts that just run from top to bottom.
A tkinter
program runs within the mainloop
. So there are only three
things you do before starting the mainloop.
- Create a window with some widgets on it.
- Create objects (variables) that hold program state.
- Define functions that can be run from the
mainloop
as callbacks or idle tasks.
A callback is called in response to activating a control (like clicking on
a button).
An idle task is started by the system after a specified number of milliseconds
when the system is not busy processing events. You can schedule idle tasks
with the Tk.after()
method.
Basically, the callbacks and idle tasks are your program.
To keep the GUI responsive, callbacks and idle tasks should not take too long; say 50 ms.
So running a complete antivirus scan in a single callback will indeed make the GUI unresponsive.
Basically, there are three possible solutions.
- Chop up the scanning process in small pieces. The first piece is executed in a callback, the rest is done in idle tasks. This is probable the most simple solution, because it makes it easy to update the GUI when you're scannning.
- Use
multiprocessing
to start a separate program to do the scanning. Set up aPipe
to enable communication between the GUI and scanning programs. Use an idle task in the GUI to read messages from the pipe and update the GUI accordingly. - Use
threading
to start a seperate thread to do the scanning. There is some confusion as to tkinter being actually safe to call from multiple threads; see the example below.
Which one works best for you hard for me to say.
Below I give examples for 1 and 3 in the form of a tkinter script to unlock ms-excel files.
First the none-threaded version:
"""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()
Then the version that uses threads:
"""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()