优化GUI程序,加入线程池,避免翻页卡死现象发生。
This commit is contained in:
@@ -515,6 +515,10 @@ class DataManagementApp:
|
|||||||
"""窗口关闭事件"""
|
"""窗口关闭事件"""
|
||||||
result = messagebox.askyesno("确认", "确定要退出系统吗?")
|
result = messagebox.askyesno("确认", "确定要退出系统吗?")
|
||||||
if result:
|
if result:
|
||||||
|
# 清理图片加载器资源
|
||||||
|
from utils.image_loader import cleanup_resources
|
||||||
|
cleanup_resources()
|
||||||
|
|
||||||
# 关闭数据库连接
|
# 关闭数据库连接
|
||||||
self.mysql_manager.close()
|
self.mysql_manager.close()
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
中间图片预览面板
|
中间图片预览面板 - 优化版本
|
||||||
|
主要优化:
|
||||||
|
1. SQLite查询移到后台线程
|
||||||
|
2. 使用线程池管理图片加载
|
||||||
|
3. 添加取消机制,避免翻页时的线程冲突
|
||||||
|
4. 正确清理图片对象,避免内存泄漏
|
||||||
"""
|
"""
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
@@ -7,15 +12,17 @@ from typing import List, Dict, Callable, Optional
|
|||||||
import threading
|
import threading
|
||||||
import sys
|
import sys
|
||||||
import os
|
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__))))
|
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 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):
|
class ImageCard(ctk.CTkFrame):
|
||||||
"""图片卡片组件"""
|
"""图片卡片组件 - 优化版本"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -36,6 +43,12 @@ class ImageCard(ctk.CTkFrame):
|
|||||||
# 勾选状态
|
# 勾选状态
|
||||||
self.check_var = ctk.BooleanVar(value=False)
|
self.check_var = ctk.BooleanVar(value=False)
|
||||||
|
|
||||||
|
# 图片加载Future对象(用于取消)
|
||||||
|
self.load_future: Optional[Future] = None
|
||||||
|
|
||||||
|
# 标记是否已销毁
|
||||||
|
self._destroyed = False
|
||||||
|
|
||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
self._load_thumbnail()
|
self._load_thumbnail()
|
||||||
|
|
||||||
@@ -61,7 +74,7 @@ class ImageCard(ctk.CTkFrame):
|
|||||||
self.image_label.pack(pady=5, padx=5)
|
self.image_label.pack(pady=5, padx=5)
|
||||||
self.image_label.bind("<Button-1>", lambda e: self.on_preview(self.data))
|
self.image_label.bind("<Button-1>", lambda e: self.on_preview(self.data))
|
||||||
|
|
||||||
# 物品名称(加粗)
|
# 物品名称(加粗)
|
||||||
name_label = ctk.CTkLabel(
|
name_label = ctk.CTkLabel(
|
||||||
self,
|
self,
|
||||||
text=self.data['goods_name'],
|
text=self.data['goods_name'],
|
||||||
@@ -111,24 +124,42 @@ class ImageCard(ctk.CTkFrame):
|
|||||||
preview_btn.pack(pady=(0, 5), padx=5)
|
preview_btn.pack(pady=(0, 5), padx=5)
|
||||||
|
|
||||||
def _load_thumbnail(self):
|
def _load_thumbnail(self):
|
||||||
"""异步加载缩略图"""
|
"""使用线程池异步加载缩略图"""
|
||||||
def load():
|
def load():
|
||||||
|
# 检查是否已销毁
|
||||||
|
if self._destroyed:
|
||||||
|
return
|
||||||
|
|
||||||
image = load_image_from_url(
|
image = load_image_from_url(
|
||||||
self.data['image_url'],
|
self.data['image_url'],
|
||||||
size=(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
|
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:
|
if image:
|
||||||
self.image_label.configure(image=image, text="")
|
self.image_label.configure(image=image, text="")
|
||||||
self.image_label.image = image
|
self.image_label.image = image # 保持引用
|
||||||
else:
|
else:
|
||||||
placeholder = create_placeholder_image(
|
placeholder = create_placeholder_image(
|
||||||
size=(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
|
size=(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
|
||||||
)
|
)
|
||||||
self.image_label.configure(image=placeholder, text="")
|
self.image_label.configure(image=placeholder, text="")
|
||||||
self.image_label.image = placeholder
|
self.image_label.image = placeholder
|
||||||
|
except Exception as e:
|
||||||
threading.Thread(target=load, daemon=True).start()
|
print(f"设置图片失败: {e}")
|
||||||
|
|
||||||
def _on_check(self):
|
def _on_check(self):
|
||||||
"""勾选框改变"""
|
"""勾选框改变"""
|
||||||
@@ -141,10 +172,24 @@ class ImageCard(ctk.CTkFrame):
|
|||||||
def set_checked(self, checked: bool):
|
def set_checked(self, checked: bool):
|
||||||
"""设置勾选状态"""
|
"""设置勾选状态"""
|
||||||
self.check_var.set(checked)
|
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):
|
class PreviewPanel(ctk.CTkFrame):
|
||||||
"""中间图片预览面板"""
|
"""中间图片预览面板 - 优化版本"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -168,6 +213,12 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
self.current_page = 1
|
self.current_page = 1
|
||||||
self.selected_ids = set()
|
self.selected_ids = set()
|
||||||
|
|
||||||
|
# 下载状态缓存(避免重复查询SQLite)
|
||||||
|
self.download_status_cache = {}
|
||||||
|
|
||||||
|
# 当前的ImageCard列表
|
||||||
|
self.image_cards: List[ImageCard] = []
|
||||||
|
|
||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
|
|
||||||
def _create_widgets(self):
|
def _create_widgets(self):
|
||||||
@@ -192,7 +243,7 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
)
|
)
|
||||||
self.count_label.pack(side="left", padx=20)
|
self.count_label.pack(side="left", padx=20)
|
||||||
|
|
||||||
# 图片网格(可滚动)
|
# 图片网格(可滚动)
|
||||||
self.scroll_frame = ctk.CTkScrollableFrame(self)
|
self.scroll_frame = ctk.CTkScrollableFrame(self)
|
||||||
self.scroll_frame.pack(fill="both", expand=True, padx=15, pady=(0, 10))
|
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.current_page = page
|
||||||
self.selected_ids.clear()
|
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_grid()
|
||||||
self._update_stats()
|
self._update_stats()
|
||||||
self._update_pagination()
|
self._update_pagination()
|
||||||
@@ -304,8 +395,7 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
def _update_grid(self):
|
def _update_grid(self):
|
||||||
"""更新图片网格"""
|
"""更新图片网格"""
|
||||||
# 清空现有卡片
|
# 清空现有卡片
|
||||||
for widget in self.grid_frame.winfo_children():
|
self._clear_grid()
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
if not self.current_data:
|
if not self.current_data:
|
||||||
no_data_label = ctk.CTkLabel(
|
no_data_label = ctk.CTkLabel(
|
||||||
@@ -322,11 +412,8 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
row = i // GRID_COLUMNS
|
row = i // GRID_COLUMNS
|
||||||
col = i % GRID_COLUMNS
|
col = i % GRID_COLUMNS
|
||||||
|
|
||||||
# 检查是否已下载
|
# 从缓存获取下载状态
|
||||||
is_downloaded = self.sqlite_manager.is_downloaded(
|
is_downloaded = self.download_status_cache.get(data['id'], False)
|
||||||
data['datasource'],
|
|
||||||
data['image_url']
|
|
||||||
)
|
|
||||||
|
|
||||||
card = ImageCard(
|
card = ImageCard(
|
||||||
self.grid_frame,
|
self.grid_frame,
|
||||||
@@ -342,11 +429,27 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
pady=CARD_PADDING,
|
pady=CARD_PADDING,
|
||||||
sticky="nsew"
|
sticky="nsew"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 保存引用
|
||||||
|
self.image_cards.append(card)
|
||||||
|
|
||||||
# 配置网格列权重
|
# 配置网格列权重
|
||||||
for col in range(GRID_COLUMNS):
|
for col in range(GRID_COLUMNS):
|
||||||
self.grid_frame.grid_columnconfigure(col, weight=1)
|
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):
|
def _update_stats(self):
|
||||||
"""更新统计信息"""
|
"""更新统计信息"""
|
||||||
selected_count = len(self.selected_ids)
|
selected_count = len(self.selected_ids)
|
||||||
@@ -378,18 +481,16 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
|
|
||||||
def _select_all(self):
|
def _select_all(self):
|
||||||
"""全选"""
|
"""全选"""
|
||||||
for widget in self.grid_frame.winfo_children():
|
for card in self.image_cards:
|
||||||
if isinstance(widget, ImageCard):
|
card.set_checked(True)
|
||||||
widget.set_checked(True)
|
self.selected_ids.add(card.data['id'])
|
||||||
self.selected_ids.add(widget.data['id'])
|
|
||||||
|
|
||||||
self._update_stats()
|
self._update_stats()
|
||||||
|
|
||||||
def _deselect_all(self):
|
def _deselect_all(self):
|
||||||
"""取消全选"""
|
"""取消全选"""
|
||||||
for widget in self.grid_frame.winfo_children():
|
for card in self.image_cards:
|
||||||
if isinstance(widget, ImageCard):
|
card.set_checked(False)
|
||||||
widget.set_checked(False)
|
|
||||||
|
|
||||||
self.selected_ids.clear()
|
self.selected_ids.clear()
|
||||||
self._update_stats()
|
self._update_stats()
|
||||||
@@ -422,15 +523,15 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
if data['id'] in self.selected_ids
|
if data['id'] in self.selected_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
# 二次确认(简化版,不显示删除范围选项)
|
# 二次确认
|
||||||
count = len(selected_data)
|
count = len(selected_data)
|
||||||
confirm = messagebox.askyesno(
|
confirm = messagebox.askyesno(
|
||||||
"确认删除",
|
"确认删除",
|
||||||
f"确定要删除选中的 {count} 张图片吗?\n\n"
|
f"确定要删除选中的 {count} 张图片吗?\n\n"
|
||||||
"此操作将:\n"
|
"此操作将:\n"
|
||||||
"1. 从MySQL数据库中删除记录\n"
|
"1. 从MySQL数据库中删除记录\n"
|
||||||
"2. 删除已下载的本地文件(如有)\n\n"
|
"2. 删除已下载的本地文件(如有)\n\n"
|
||||||
"删除后无法恢复!",
|
"删除后无法恢复!",
|
||||||
icon='warning'
|
icon='warning'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -471,7 +572,7 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
print(f"删除记录失败 (ID: {data['id']}): {e}")
|
print(f"删除记录失败 (ID: {data['id']}): {e}")
|
||||||
failed_ids.append(data['id'])
|
failed_ids.append(data['id'])
|
||||||
|
|
||||||
# 更新UI(在主线程)
|
# 更新UI(在主线程)
|
||||||
self.after(0, lambda: self._on_delete_complete(success_count, len(failed_ids)))
|
self.after(0, lambda: self._on_delete_complete(success_count, len(failed_ids)))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -494,7 +595,7 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
# 清空选中
|
# 清空选中
|
||||||
self.selected_ids.clear()
|
self.selected_ids.clear()
|
||||||
|
|
||||||
# 调用删除回调,刷新数据
|
# 调用删除回调,刷新数据
|
||||||
if self.on_delete_callback:
|
if self.on_delete_callback:
|
||||||
self.on_delete_callback()
|
self.on_delete_callback()
|
||||||
|
|
||||||
@@ -559,20 +660,20 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
def _first_page(self):
|
def _first_page(self):
|
||||||
"""首页"""
|
"""首页"""
|
||||||
if self.current_page > 1:
|
if self.current_page > 1:
|
||||||
# 需要重新查询数据(在主应用中处理)
|
# 需要重新查询数据(在主应用中处理)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _prev_page(self):
|
def _prev_page(self):
|
||||||
"""上一页"""
|
"""上一页"""
|
||||||
if self.current_page > 1:
|
if self.current_page > 1:
|
||||||
# 需要重新查询数据(在主应用中处理)
|
# 需要重新查询数据(在主应用中处理)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _next_page(self):
|
def _next_page(self):
|
||||||
"""下一页"""
|
"""下一页"""
|
||||||
total_pages = (self.total_count + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE
|
total_pages = (self.total_count + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE
|
||||||
if self.current_page < total_pages:
|
if self.current_page < total_pages:
|
||||||
# 需要重新查询数据(在主应用中处理)
|
# 需要重新查询数据(在主应用中处理)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_current_page(self) -> int:
|
def get_current_page(self) -> int:
|
||||||
@@ -585,6 +686,7 @@ class PreviewPanel(ctk.CTkFrame):
|
|||||||
self.total_count = 0
|
self.total_count = 0
|
||||||
self.current_page = 1
|
self.current_page = 1
|
||||||
self.selected_ids.clear()
|
self.selected_ids.clear()
|
||||||
self._update_grid()
|
self.download_status_cache.clear()
|
||||||
|
self._clear_grid()
|
||||||
self._update_stats()
|
self._update_stats()
|
||||||
self._update_pagination()
|
self._update_pagination()
|
||||||
|
|||||||
@@ -1,40 +1,92 @@
|
|||||||
"""
|
"""
|
||||||
图片加载工具
|
图片加载工具
|
||||||
用于从URL加载图片并转换为PIL/CTkImage格式
|
用于从URL加载图片并转换为PIL/CTkImage格式
|
||||||
|
优化版本:使用连接池、线程池和缓存
|
||||||
"""
|
"""
|
||||||
import io
|
import io
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from functools import lru_cache
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
# ======================== 全局资源管理 ========================
|
||||||
|
|
||||||
|
# HTTP会话(连接池)
|
||||||
|
_session = None
|
||||||
|
_session_lock = threading.Lock()
|
||||||
|
|
||||||
|
def get_session() -> requests.Session:
|
||||||
|
"""获取全局HTTP会话(单例模式)"""
|
||||||
|
global _session
|
||||||
|
if _session is None:
|
||||||
|
with _session_lock:
|
||||||
|
if _session is None:
|
||||||
|
_session = requests.Session()
|
||||||
|
# 配置连接池
|
||||||
|
adapter = requests.adapters.HTTPAdapter(
|
||||||
|
pool_connections=10, # 连接池大小
|
||||||
|
pool_maxsize=20, # 最大连接数
|
||||||
|
max_retries=2, # 重试次数
|
||||||
|
pool_block=False
|
||||||
|
)
|
||||||
|
_session.mount('http://', adapter)
|
||||||
|
_session.mount('https://', adapter)
|
||||||
|
return _session
|
||||||
|
|
||||||
|
|
||||||
|
# 线程池(用于图片加载)
|
||||||
|
_thread_pool = None
|
||||||
|
_thread_pool_lock = threading.Lock()
|
||||||
|
|
||||||
|
def get_thread_pool() -> ThreadPoolExecutor:
|
||||||
|
"""获取全局线程池(单例模式)"""
|
||||||
|
global _thread_pool
|
||||||
|
if _thread_pool is None:
|
||||||
|
with _thread_pool_lock:
|
||||||
|
if _thread_pool is None:
|
||||||
|
# 限制最多同时加载10张图片
|
||||||
|
_thread_pool = ThreadPoolExecutor(
|
||||||
|
max_workers=10,
|
||||||
|
thread_name_prefix="ImageLoader"
|
||||||
|
)
|
||||||
|
return _thread_pool
|
||||||
|
|
||||||
|
|
||||||
|
# ======================== 图片加载函数 ========================
|
||||||
|
|
||||||
def load_image_from_url(
|
def load_image_from_url(
|
||||||
url: str,
|
url: str,
|
||||||
size: tuple = (150, 150),
|
size: tuple = (150, 150),
|
||||||
timeout: int = 10
|
timeout: int = 5 # 降低超时时间,避免长时间阻塞
|
||||||
) -> Optional[ctk.CTkImage]:
|
) -> Optional[ctk.CTkImage]:
|
||||||
"""
|
"""
|
||||||
从URL加载图片
|
从URL加载图片(使用连接池)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: 图片URL
|
url: 图片URL
|
||||||
size: 目标尺寸 (width, height)
|
size: 目标尺寸 (width, height)
|
||||||
timeout: 超时时间(秒)
|
timeout: 超时时间(秒)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
CTkImage对象,失败返回None
|
CTkImage对象,失败返回None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# 使用全局session(连接复用)
|
||||||
|
session = get_session()
|
||||||
|
|
||||||
# 下载图片
|
# 下载图片
|
||||||
response = requests.get(url, timeout=timeout)
|
response = session.get(url, timeout=timeout)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# 转换为PIL Image
|
# 转换为PIL Image
|
||||||
image_data = io.BytesIO(response.content)
|
image_data = io.BytesIO(response.content)
|
||||||
pil_image = Image.open(image_data)
|
pil_image = Image.open(image_data)
|
||||||
|
|
||||||
# 转换为RGB(防止RGBA等格式问题)
|
# 转换为RGB(防止RGBA等格式问题)
|
||||||
if pil_image.mode != 'RGB':
|
if pil_image.mode != 'RGB':
|
||||||
pil_image = pil_image.convert('RGB')
|
pil_image = pil_image.convert('RGB')
|
||||||
|
|
||||||
@@ -47,17 +99,24 @@ def load_image_from_url(
|
|||||||
|
|
||||||
return ctk_image
|
return ctk_image
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print(f"加载图片超时: {url}")
|
||||||
|
return None
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"网络请求失败 {url}: {e}")
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"加载图片失败 {url}: {e}")
|
print(f"加载图片失败 {url}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=100)
|
||||||
def create_placeholder_image(
|
def create_placeholder_image(
|
||||||
size: tuple = (150, 150),
|
size: tuple = (150, 150),
|
||||||
text: str = "加载失败"
|
text: str = "加载失败"
|
||||||
) -> ctk.CTkImage:
|
) -> ctk.CTkImage:
|
||||||
"""
|
"""
|
||||||
创建占位图片
|
创建占位图片(带缓存)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
size: 图片尺寸
|
size: 图片尺寸
|
||||||
@@ -76,3 +135,18 @@ def create_placeholder_image(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return ctk_image
|
return ctk_image
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_resources():
|
||||||
|
"""清理全局资源(程序退出时调用)"""
|
||||||
|
global _session, _thread_pool
|
||||||
|
|
||||||
|
# 关闭线程池
|
||||||
|
if _thread_pool is not None:
|
||||||
|
_thread_pool.shutdown(wait=False)
|
||||||
|
_thread_pool = None
|
||||||
|
|
||||||
|
# 关闭HTTP会话
|
||||||
|
if _session is not None:
|
||||||
|
_session.close()
|
||||||
|
_session = None
|
||||||
|
|||||||
Reference in New Issue
Block a user