Kivy GUI按钮触发下载代码时界面阻塞问题求助
Hey there! Let's tackle that GUI freeze issue you're facing. The core problem here is that your download logic (including pyppeteer operations) is running directly on Kivy's main UI thread, which blocks all interactive elements like window resizing or button clicks until the task finishes. Here's a step-by-step solution using threads and Kivy's Clock to keep your UI responsive while showing download progress:
Step 1: Update Your GUI Code (gui.py)
We'll move the download task to a background thread and use Kivy's Clock to safely update the UI from the main thread:
from kivy.app import App from kivy.uix.floatlayout import FloatLayout from kivy.clock import Clock from kivy.uix.popup import Popup from kivy.uix.label import Label import threading import asyncio from download import Downloader class MainLayout(FloatLayout): def on_download(self): # Reset progress bar and validate input URL self.ids.progress_bar.value = 0 url = self.ids.url_input.text if not url.strip(): self.show_error("Please enter a valid URL!") return downloader = Downloader() # Launch download task in a background daemon thread threading.Thread( target=self._run_download_task, args=(downloader, url), daemon=True ).start() def _run_download_task(self, downloader, url): # Run async pyppeteer logic in a separate asyncio loop (sub-thread) try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(downloader.download_images(url, self.update_progress)) # Notify UI when download finishes Clock.schedule_once(lambda dt: self.show_download_complete()) except Exception as e: # Pass errors back to UI thread Clock.schedule_once(lambda dt: self.show_error(str(e))) def update_progress(self, progress): # Schedule UI update on main thread (required for Kivy widgets) Clock.schedule_once(lambda dt: self._update_progress_ui(progress)) def _update_progress_ui(self, progress): self.ids.progress_bar.value = progress def show_download_complete(self): popup = Popup( title="Success!", content=Label(text="Images downloaded successfully!"), size_hint=(0.5, 0.3) ) popup.open() def show_error(self, error_msg): popup = Popup( title="Oops, Error!", content=Label(text=f"Download failed: {error_msg}"), size_hint=(0.5, 0.3) ) popup.open() class DownloaderApp(App): pass DownloaderApp().run()
Step 2: Add a Progress Bar to Your KV File (downloader.kv)
We'll add a ProgressBar to visualize download progress:
MainLayout: <MainLayout>: BoxLayout: orientation: "vertical" padding: "10dp" spacing: "10dp" TextInput: id: url_input hint_text: "Enter URL" write_tab: False multiline: False on_text_validate: root.on_download() Button: id: download_button text: "Download" width: "150dp" size_hint: None, 1 on_press: root.on_download() ProgressBar: id: progress_bar size_hint: 1, 0.1 value: 0
Step 3: Modify the Downloader to Support Progress Callbacks (downloader.py)
Update your Downloader class to accept a progress callback and update it during the download process:
from pyppeteer import launch import aiohttp import os class Downloader: def __init__(self): # Create downloads directory if it doesn't exist os.makedirs("./downloads", exist_ok=True) async def download_images(self, url, progress_callback): browser = await launch(headless=True) page = await browser.newPage() await page.goto(url, {'waitUntil': 'domcontentloaded'}) # Get all image URLs from the page (replace with your actual script) script = self.get_script() image_urls = await page.evaluate(script) total_images = len(image_urls) if total_images == 0: progress_callback(100) await browser.close() return # Download each image asynchronously and update progress async with aiohttp.ClientSession() as session: for index, img_url in enumerate(image_urls): try: await self._download_single_image(session, img_url) except Exception as e: print(f"Failed to download {img_url}: {e}") # Calculate and send progress update progress = ((index + 1) / total_images) * 100 progress_callback(progress) await browser.close() async def _download_single_image(self, session, img_url): # Save image to downloads directory async with session.get(img_url) as response: if response.status == 200: filename = img_url.split("/")[-1].split("?")[0] # Clean filename with open(f"./downloads/{filename}", "wb") as f: f.write(await response.read()) def get_script(self): # Script to extract all image URLs from the page return """ return Array.from(document.querySelectorAll('img')).map(img => img.src); """ def convert_images(self, images): # TODO: Implement your image conversion logic here pass
Key Explanations
- Background Threads: We use
threading.Threadto offload the entire download task (including pyppeteer's async operations) to a background thread. This keeps Kivy's main UI thread free to handle interactions like window movement. - Asyncio in Sub-Threads: Pyppeteer requires an asyncio event loop, so we create a new loop in the background thread to avoid conflicts with Kivy's main event loop.
- Safe UI Updates: Kivy doesn't allow modifying UI widgets from non-main threads. We use
Clock.schedule_onceto queue progress updates and popups to run on the main thread. - Progress Tracking: The
progress_callbackfunction lets the downloader send progress updates back to the UI, which we use to update theProgressBar.
内容的提问来源于stack exchange,提问作者link

