如何在Tkinter图片下载GUI应用中创建线程避免界面未响应?
Fix Tkinter GUI "Not Responding" During Image Downloads with Threading
Hey there! The issue you're hitting is super common with Tkinter: it runs on a single main thread, so any long-running blocking task (like downloading images) will freeze the GUI because it blocks the event loop that handles button clicks, window updates, etc. Let's fix this by offloading the download work to a separate thread, while keeping all GUI interactions safely in the main thread.
Here's how to adjust your code step by step:
Key Changes Needed
- Move download logic to a dedicated thread function: This keeps the main thread free to handle GUI interactions.
- Use Tkinter's
after()method for GUI updates: You can't modify Tkinter widgets directly from a child thread—after()lets you schedule GUI updates to run in the main thread safely.
Modified Working Code
from tkinter import * import requests import os from threading import Thread from urllib.request import urlretrieve class Gui(Tk): def __init__(self): self.do_splash_call = False Tk.__init__(self) self.title('unsplash image download') self.geometry('900x500') # main canvas main = Canvas(self, bg='#0099e6') main.place(relx=0, rely=0, relwidth=1, relheight=1) # frame1 for upper left name_frame = Frame(main) name_frame.place(relx=.1, rely=.1) # name input and label name_label = Label(name_frame, text='Image Name:') name_label.pack(side=LEFT, pady=3) self.name = Entry(name_frame, bd=0, font=2, width=23) self.name.pack(side=RIGHT, pady=3) # frame2 for upper right page_frame = Frame(main) page_frame.place(relx=.5, rely=.1, relwidth=.3) # landing page Label(page_frame, text='Landing page:').pack(side=LEFT, pady=3) self.page = Spinbox(page_frame, from_=1, to=50, width=30) self.page.pack(side=RIGHT, pady=3) # frame3 for down left size_frame = Frame(main) size_frame.place(relx=.1, rely=.2) # name input and label size_label = Label(size_frame, text='Android, pc, tablet:') size_label.pack(side=LEFT, pady=3) self.size = Entry(size_frame, bd=0, font=2) self.size.pack(side=RIGHT, pady=3) # button download = Button(main, bd=1, font=4, text='Download', width=10, command=self.splash) download.place(relx=.6, rely=.2) # output self.output = Text(main, bd=2) self.output.place(relx=0, rely=.5, relheight=1, relwidth=1) # path to save images save_frame = Frame(main) save_frame.place(relx=.1, rely=.3, relwidth=.7) Label(save_frame, text='Save:').pack(side=LEFT, padx=5) self.save = Entry(save_frame, bd=1) self.save.place(relx=.1, relwidth=1) # insert path for save images self.save.insert(0, str('c:\\users\\abc\\Desktop\\photos')) def path(self): save_path = self.save.get() if not os.path.exists(save_path): os.makedirs(save_path) os.chdir(save_path) def update_output(self, message, is_error=False): # Helper to safely update Text widget from main thread def _safe_update(): self.output.insert(INSERT, f'\n{message}') if is_error: self.output.tag_add('error', 'end-2l', 'end-1l') self.output.tag_config('error', background='#ff4d4d', foreground='black') else: self.output.tag_add('fine', 'end-2l', 'end-1l') self.output.tag_config('fine', background='lightGreen', foreground='#196619') # Auto-scroll to latest message self.output.see(END) # Schedule update to run in main thread self.after(0, _safe_update) def download_images(self): # This runs in a separate background thread try: # Get user inputs image_name = self.name.get().strip() pages = self.page.get() size_input = self.size.get().strip().lower() # Set image size/orientation based on input if size_input in ['pc', 'desktop']: size = 1080 orientation = 'landscape' elif size_input in ['hd', 'full hd', 'clear', 'normal']: size = 1500 orientation = 'landscape' elif size_input in ['android', 'mini', 'mobile']: size = 400 orientation = 'portrait' else: size = 1500 orientation = 'landscape' # Fixed API URL (removed HTML entities & and extra spaces) api = f'https://api.unsplash.com/photos/search?query={image_name}&resolution={size}&orientation={orientation}&client_id=YOUR_UNSPLASH_API_KEY&page={pages}&w=1500&dpi=2' res = requests.get(api).json() # Handle API errors (like invalid key) if 'errors' in res: self.update_output(f"API Error: {','.join(res['errors'])}", is_error=True) return # Prepare save path self.path() # Download first 2 images for idx, photo in enumerate(res[:2]): url = photo['links']['download'] name_of_image = photo.get('alt_description', f'image_{idx+1}') img_name = '_'.join(name_of_image[:40].split(' ')) + '.png' self.update_output(f'Downloading img.... {img_name}') urlretrieve(url, img_name) self.update_output(f'Successfully downloaded: {img_name}') except Exception as e: self.update_output(f'Error: {str(e)}', is_error=True) print(e) def splash(self): # Start the download thread (daemon=True means it exits when GUI closes) download_thread = Thread(target=self.download_images, daemon=True) download_thread.start() if __name__ == '__main__': app = Gui() app.mainloop()
What's Different?
download_images()method: All the download logic lives here, running in a separate daemon thread. Daemon threads automatically terminate when the main GUI window closes, which is perfect for background tasks.update_output()helper: Usesself.after(0, ...)to safely update the Text widget from the main thread—Tkinter isn't thread-safe for widget modifications, so this is non-negotiable.- Fixed API URL: Replaced
&with&(HTML entities don't belong in raw URLs) and cleaned up extra spaces to avoid API errors. - Simplified
splash(): Now it just starts the download thread instead of running blocking code directly, keeping the GUI responsive.
Important Reminders
- Replace
YOUR_UNSPLASH_API_KEYwith your actual Unsplash API key. - Never modify Tkinter widgets directly from child threads—always use
after()to schedule updates in the main thread. - The daemon thread ensures your download task won't hang around if you close the GUI window early.
内容的提问来源于stack exchange,提问作者Rohit Dalal




