""" 中间图片预览面板 """ import customtkinter as ctk from tkinter import messagebox from typing import List, Dict, Callable, Optional import threading import sys import os # 添加父目录到路径 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 class ImageCard(ctk.CTkFrame): """图片卡片组件""" def __init__( self, parent, data: Dict, is_downloaded: bool, on_check_changed: Callable, on_preview: Callable, **kwargs ): super().__init__(parent, **kwargs) self.data = data self.is_downloaded = is_downloaded self.on_check_changed = on_check_changed self.on_preview = on_preview # 勾选状态 self.check_var = ctk.BooleanVar(value=False) self._create_widgets() self._load_thumbnail() def _create_widgets(self): """创建卡片组件""" # 勾选框 self.checkbox = ctk.CTkCheckBox( self, text="", variable=self.check_var, command=self._on_check, width=20 ) self.checkbox.pack(pady=(5, 0), padx=5, anchor="w") # 图片容器 self.image_label = ctk.CTkLabel( self, text="加载中...", width=THUMBNAIL_SIZE, height=THUMBNAIL_SIZE ) self.image_label.pack(pady=5, padx=5) self.image_label.bind("", lambda e: self.on_preview(self.data)) # 物品名称(加粗) name_label = ctk.CTkLabel( self, text=self.data['goods_name'], font=("Arial", 11, "bold"), wraplength=THUMBNAIL_SIZE - 10 ) name_label.pack(pady=(0, 2), padx=5) # 采集时间 time_label = ctk.CTkLabel( self, text=self.data['create_time'], font=("Arial", 9), text_color="gray" ) time_label.pack(pady=(0, 2), padx=5) # 设备编码 if self.data['equipment_code']: equipment_label = ctk.CTkLabel( self, text=f"设备: {self.data['equipment_code']}", font=("Arial", 9), text_color="gray" ) equipment_label.pack(pady=(0, 2), padx=5) # 已下载标记 if self.is_downloaded: downloaded_label = ctk.CTkLabel( self, text="✅ 已下载", font=("Arial", 10), text_color="green" ) downloaded_label.pack(pady=(0, 5), padx=5) # 查看大图按钮 preview_btn = ctk.CTkButton( self, text="查看大图", command=lambda: self.on_preview(self.data), width=100, height=25, font=("Arial", 10) ) preview_btn.pack(pady=(0, 5), padx=5) def _load_thumbnail(self): """异步加载缩略图""" def load(): image = load_image_from_url( self.data['image_url'], size=(THUMBNAIL_SIZE, THUMBNAIL_SIZE) ) if image: self.image_label.configure(image=image, text="") 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() def _on_check(self): """勾选框改变""" self.on_check_changed(self.data['id'], self.check_var.get()) def is_checked(self) -> bool: """是否被勾选""" return self.check_var.get() def set_checked(self, checked: bool): """设置勾选状态""" self.check_var.set(checked) class PreviewPanel(ctk.CTkFrame): """中间图片预览面板""" def __init__( self, parent, sqlite_manager, on_download_callback: Callable, **kwargs ): super().__init__(parent, **kwargs) self.sqlite_manager = sqlite_manager self.on_download_callback = on_download_callback # 数据 self.current_data = [] self.total_count = 0 self.current_page = 1 self.selected_ids = set() self._create_widgets() def _create_widgets(self): """创建UI组件""" # 标题栏 header_frame = ctk.CTkFrame(self) header_frame.pack(fill="x", padx=15, pady=(15, 10)) title_label = ctk.CTkLabel( header_frame, text="图片预览", font=("Arial", 16, "bold") ) title_label.pack(side="left", padx=10) # 统计信息 self.count_label = ctk.CTkLabel( header_frame, text="共 0 张 | 已选 0 张", font=("Arial", 12), text_color="gray" ) 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)) # 网格容器 self.grid_frame = ctk.CTkFrame(self.scroll_frame) self.grid_frame.pack(fill="both", expand=True) # 底部操作栏 bottom_frame = ctk.CTkFrame(self) bottom_frame.pack(fill="x", padx=15, pady=(0, 15)) # 左侧按钮 left_buttons = ctk.CTkFrame(bottom_frame) left_buttons.pack(side="left", padx=10, pady=10) self.select_all_btn = ctk.CTkButton( left_buttons, text="全选", command=self._select_all, width=80, height=30 ) self.select_all_btn.pack(side="left", padx=5) self.deselect_all_btn = ctk.CTkButton( left_buttons, text="取消全选", command=self._deselect_all, width=80, height=30, fg_color="gray", hover_color="darkgray" ) self.deselect_all_btn.pack(side="left", padx=5) self.download_btn = ctk.CTkButton( left_buttons, text="批量下载", command=self._on_download, width=100, height=30, fg_color="green", hover_color="darkgreen" ) self.download_btn.pack(side="left", padx=5) # 分页控件 page_frame = ctk.CTkFrame(bottom_frame) page_frame.pack(side="right", padx=10, pady=10) self.prev_btn = ctk.CTkButton( page_frame, text="上一页", command=self._prev_page, width=80, height=30 ) self.prev_btn.pack(side="left", padx=5) self.page_label = ctk.CTkLabel( page_frame, text="第 1 页", font=("Arial", 11) ) self.page_label.pack(side="left", padx=10) self.next_btn = ctk.CTkButton( page_frame, text="下一页", command=self._next_page, width=80, height=30 ) self.next_btn.pack(side="left", padx=5) def load_data(self, data_list: List[Dict], total_count: int, page: int = 1): """加载图片数据""" self.current_data = data_list self.total_count = total_count self.current_page = page self.selected_ids.clear() self._update_grid() self._update_stats() self._update_pagination() def _update_grid(self): """更新图片网格""" # 清空现有卡片 for widget in self.grid_frame.winfo_children(): widget.destroy() if not self.current_data: no_data_label = ctk.CTkLabel( self.grid_frame, text="暂无数据", font=("Arial", 14), text_color="gray" ) no_data_label.grid(row=0, column=0, pady=50, padx=50) return # 创建网格卡片 for i, data in enumerate(self.current_data): row = i // GRID_COLUMNS col = i % GRID_COLUMNS # 检查是否已下载 is_downloaded = self.sqlite_manager.is_downloaded( data['datasource'], data['image_url'] ) card = ImageCard( self.grid_frame, data=data, is_downloaded=is_downloaded, on_check_changed=self._on_check_changed, on_preview=self._on_preview ) card.grid( row=row, column=col, padx=CARD_PADDING, pady=CARD_PADDING, sticky="nsew" ) # 配置网格列权重 for col in range(GRID_COLUMNS): self.grid_frame.grid_columnconfigure(col, weight=1) def _update_stats(self): """更新统计信息""" selected_count = len(self.selected_ids) self.count_label.configure( text=f"共 {self.total_count} 张 | 已选 {selected_count} 张" ) def _update_pagination(self): """更新分页控件""" total_pages = (self.total_count + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE if total_pages == 0: total_pages = 1 self.page_label.configure(text=f"第 {self.current_page}/{total_pages} 页") # 更新按钮状态 self.prev_btn.configure(state="normal" if self.current_page > 1 else "disabled") self.next_btn.configure(state="normal" if self.current_page < total_pages else "disabled") def _on_check_changed(self, item_id: int, checked: bool): """勾选框改变""" if checked: self.selected_ids.add(item_id) else: self.selected_ids.discard(item_id) self._update_stats() 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']) self._update_stats() def _deselect_all(self): """取消全选""" for widget in self.grid_frame.winfo_children(): if isinstance(widget, ImageCard): widget.set_checked(False) self.selected_ids.clear() self._update_stats() def _on_download(self): """批量下载""" if not self.selected_ids: messagebox.showwarning("提示", "请先选择要下载的图片") return # 获取选中的图片数据 selected_data = [ data for data in self.current_data if data['id'] in self.selected_ids ] # 调用下载回调 if self.on_download_callback: self.on_download_callback(selected_data) def _on_preview(self, data: Dict): """查看大图""" # 创建预览窗口 preview_window = ctk.CTkToplevel(self) preview_window.title(f"预览 - {data['goods_name']}") preview_window.geometry("800x700") # 窗口属性 preview_window.transient(self) preview_window.grab_set() preview_window.lift() preview_window.focus_set() # 居中显示 preview_window.update_idletasks() x = (preview_window.winfo_screenwidth() // 2) - 400 y = (preview_window.winfo_screenheight() // 2) - 350 preview_window.geometry(f"800x700+{x}+{y}") # 信息框架 info_frame = ctk.CTkFrame(preview_window) info_frame.pack(fill="x", padx=20, pady=(20, 10)) info_text = f""" 物品名称: {data['goods_name']} 采集时间: {data['create_time']} 设备编码: {data['equipment_code']} 数据源: {data['datasource']} """.strip() info_label = ctk.CTkLabel( info_frame, text=info_text, font=("Arial", 12), justify="left" ) info_label.pack(pady=10, padx=10) # 图片容器 image_container = ctk.CTkLabel( preview_window, text="加载中...", width=760, height=550 ) image_container.pack(pady=10, padx=20) # 异步加载大图 def load_large_image(): image = load_image_from_url(data['image_url'], size=(760, 550)) if image: image_container.configure(image=image, text="") image_container.image = image else: image_container.configure(text="图片加载失败") threading.Thread(target=load_large_image, daemon=True).start() 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: """获取当前页码""" return self.current_page def clear(self): """清空数据""" self.current_data = [] self.total_count = 0 self.current_page = 1 self.selected_ids.clear() self._update_grid() self._update_stats() self._update_pagination()