优化GUI程序,加入线程池,避免翻页卡死现象发生。

This commit is contained in:
2025-12-03 14:52:37 +08:00
parent 40e90ac7c2
commit 5b31827740
3 changed files with 221 additions and 41 deletions
+136 -34
View File
@@ -1,5 +1,10 @@
"""
中间图片预览面板
中间图片预览面板 - 优化版本
主要优化:
1. SQLite查询移到后台线程
2. 使用线程池管理图片加载
3. 添加取消机制,避免翻页时的线程冲突
4. 正确清理图片对象,避免内存泄漏
"""
import customtkinter as ctk
from tkinter import messagebox
@@ -7,15 +12,17 @@ from typing import List, Dict, Callable, Optional
import threading
import sys
import os
from concurrent.futures import ThreadPoolExecutor, Future
from collections import defaultdict
# 添加父目录到路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import GRID_COLUMNS, THUMBNAIL_SIZE, CARD_PADDING, ITEMS_PER_PAGE
from utils.image_loader import load_image_from_url, create_placeholder_image
from utils.image_loader import load_image_from_url, create_placeholder_image, get_thread_pool
class ImageCard(ctk.CTkFrame):
"""图片卡片组件"""
"""图片卡片组件 - 优化版本"""
def __init__(
self,
@@ -36,6 +43,12 @@ class ImageCard(ctk.CTkFrame):
# 勾选状态
self.check_var = ctk.BooleanVar(value=False)
# 图片加载Future对象(用于取消)
self.load_future: Optional[Future] = None
# 标记是否已销毁
self._destroyed = False
self._create_widgets()
self._load_thumbnail()
@@ -61,7 +74,7 @@ class ImageCard(ctk.CTkFrame):
self.image_label.pack(pady=5, padx=5)
self.image_label.bind("<Button-1>", lambda e: self.on_preview(self.data))
# 物品名称加粗
# 物品名称(加粗)
name_label = ctk.CTkLabel(
self,
text=self.data['goods_name'],
@@ -111,24 +124,42 @@ class ImageCard(ctk.CTkFrame):
preview_btn.pack(pady=(0, 5), padx=5)
def _load_thumbnail(self):
"""异步加载缩略图"""
"""使用线程池异步加载缩略图"""
def load():
# 检查是否已销毁
if self._destroyed:
return
image = load_image_from_url(
self.data['image_url'],
size=(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
)
# 在主线程更新UI
if not self._destroyed:
self.after(0, lambda: self._set_image(image))
# 使用全局线程池
thread_pool = get_thread_pool()
self.load_future = thread_pool.submit(load)
def _set_image(self, image):
"""设置图片(在主线程调用)"""
if self._destroyed:
return
try:
if image:
self.image_label.configure(image=image, text="")
self.image_label.image = image
self.image_label.image = image # 保持引用
else:
placeholder = create_placeholder_image(
size=(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
)
self.image_label.configure(image=placeholder, text="")
self.image_label.image = placeholder
threading.Thread(target=load, daemon=True).start()
except Exception as e:
print(f"设置图片失败: {e}")
def _on_check(self):
"""勾选框改变"""
@@ -141,10 +172,24 @@ class ImageCard(ctk.CTkFrame):
def set_checked(self, checked: bool):
"""设置勾选状态"""
self.check_var.set(checked)
def cancel_loading(self):
"""取消图片加载"""
if self.load_future and not self.load_future.done():
self.load_future.cancel()
def cleanup(self):
"""清理资源"""
self._destroyed = True
self.cancel_loading()
# 清理图片引用
if hasattr(self.image_label, 'image'):
self.image_label.image = None
class PreviewPanel(ctk.CTkFrame):
"""中间图片预览面板"""
"""中间图片预览面板 - 优化版本"""
def __init__(
self,
@@ -168,6 +213,12 @@ class PreviewPanel(ctk.CTkFrame):
self.current_page = 1
self.selected_ids = set()
# 下载状态缓存(避免重复查询SQLite)
self.download_status_cache = {}
# 当前的ImageCard列表
self.image_cards: List[ImageCard] = []
self._create_widgets()
def _create_widgets(self):
@@ -192,7 +243,7 @@ class PreviewPanel(ctk.CTkFrame):
)
self.count_label.pack(side="left", padx=20)
# 图片网格可滚动
# 图片网格(可滚动)
self.scroll_frame = ctk.CTkScrollableFrame(self)
self.scroll_frame.pack(fill="both", expand=True, padx=15, pady=(0, 10))
@@ -297,6 +348,46 @@ class PreviewPanel(ctk.CTkFrame):
self.current_page = page
self.selected_ids.clear()
# 在后台线程查询下载状态
def query_download_status():
try:
# 批量查询下载状态
status_dict = {}
for data in data_list:
is_downloaded = self.sqlite_manager.is_downloaded(
data['datasource'],
data['image_url']
)
status_dict[data['id']] = is_downloaded
# 在主线程更新UI
self.after(0, lambda: self._update_grid_with_status(status_dict))
except Exception as e:
print(f"查询下载状态失败: {e}")
# 失败时使用空字典
self.after(0, lambda: self._update_grid_with_status({}))
# 先显示加载中,然后在后台查询
self._show_loading()
threading.Thread(target=query_download_status, daemon=True).start()
def _show_loading(self):
"""显示加载中状态"""
# 清空现有卡片
self._clear_grid()
loading_label = ctk.CTkLabel(
self.grid_frame,
text="加载中...",
font=("Arial", 14),
text_color="gray"
)
loading_label.grid(row=0, column=0, pady=50, padx=50)
def _update_grid_with_status(self, status_dict: Dict[int, bool]):
"""使用下载状态更新网格"""
self.download_status_cache = status_dict
self._update_grid()
self._update_stats()
self._update_pagination()
@@ -304,8 +395,7 @@ class PreviewPanel(ctk.CTkFrame):
def _update_grid(self):
"""更新图片网格"""
# 清空现有卡片
for widget in self.grid_frame.winfo_children():
widget.destroy()
self._clear_grid()
if not self.current_data:
no_data_label = ctk.CTkLabel(
@@ -322,11 +412,8 @@ class PreviewPanel(ctk.CTkFrame):
row = i // GRID_COLUMNS
col = i % GRID_COLUMNS
# 检查是否已下载
is_downloaded = self.sqlite_manager.is_downloaded(
data['datasource'],
data['image_url']
)
# 从缓存获取下载状态
is_downloaded = self.download_status_cache.get(data['id'], False)
card = ImageCard(
self.grid_frame,
@@ -342,11 +429,27 @@ class PreviewPanel(ctk.CTkFrame):
pady=CARD_PADDING,
sticky="nsew"
)
# 保存引用
self.image_cards.append(card)
# 配置网格列权重
for col in range(GRID_COLUMNS):
self.grid_frame.grid_columnconfigure(col, weight=1)
def _clear_grid(self):
"""清空网格(正确清理资源)"""
# 先取消所有图片加载
for card in self.image_cards:
card.cleanup()
# 清空列表
self.image_cards.clear()
# 销毁所有子组件
for widget in self.grid_frame.winfo_children():
widget.destroy()
def _update_stats(self):
"""更新统计信息"""
selected_count = len(self.selected_ids)
@@ -378,18 +481,16 @@ class PreviewPanel(ctk.CTkFrame):
def _select_all(self):
"""全选"""
for widget in self.grid_frame.winfo_children():
if isinstance(widget, ImageCard):
widget.set_checked(True)
self.selected_ids.add(widget.data['id'])
for card in self.image_cards:
card.set_checked(True)
self.selected_ids.add(card.data['id'])
self._update_stats()
def _deselect_all(self):
"""取消全选"""
for widget in self.grid_frame.winfo_children():
if isinstance(widget, ImageCard):
widget.set_checked(False)
for card in self.image_cards:
card.set_checked(False)
self.selected_ids.clear()
self._update_stats()
@@ -422,15 +523,15 @@ class PreviewPanel(ctk.CTkFrame):
if data['id'] in self.selected_ids
]
# 二次确认(简化版,不显示删除范围选项)
# 二次确认
count = len(selected_data)
confirm = messagebox.askyesno(
"确认删除",
f"确定要删除选中的 {count} 张图片吗\n\n"
f"确定要删除选中的 {count} 张图片吗?\n\n"
"此操作将:\n"
"1. 从MySQL数据库中删除记录\n"
"2. 删除已下载的本地文件如有\n\n"
"删除后无法恢复",
"2. 删除已下载的本地文件(如有)\n\n"
"删除后无法恢复!",
icon='warning'
)
@@ -471,7 +572,7 @@ class PreviewPanel(ctk.CTkFrame):
print(f"删除记录失败 (ID: {data['id']}): {e}")
failed_ids.append(data['id'])
# 更新UI在主线程
# 更新UI(在主线程)
self.after(0, lambda: self._on_delete_complete(success_count, len(failed_ids)))
except Exception as e:
@@ -494,7 +595,7 @@ class PreviewPanel(ctk.CTkFrame):
# 清空选中
self.selected_ids.clear()
# 调用删除回调刷新数据
# 调用删除回调,刷新数据
if self.on_delete_callback:
self.on_delete_callback()
@@ -559,20 +660,20 @@ class PreviewPanel(ctk.CTkFrame):
def _first_page(self):
"""首页"""
if self.current_page > 1:
# 需要重新查询数据在主应用中处理
# 需要重新查询数据(在主应用中处理)
pass
def _prev_page(self):
"""上一页"""
if self.current_page > 1:
# 需要重新查询数据在主应用中处理
# 需要重新查询数据(在主应用中处理)
pass
def _next_page(self):
"""下一页"""
total_pages = (self.total_count + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE
if self.current_page < total_pages:
# 需要重新查询数据在主应用中处理
# 需要重新查询数据(在主应用中处理)
pass
def get_current_page(self) -> int:
@@ -585,6 +686,7 @@ class PreviewPanel(ctk.CTkFrame):
self.total_count = 0
self.current_page = 1
self.selected_ids.clear()
self._update_grid()
self.download_status_cache.clear()
self._clear_grid()
self._update_stats()
self._update_pagination()