"""
设置预览图有进度条版本
"""
import wx
import os
import cv2
import numpy as np
from PIL import Image
import random
from pathlib import Path
from moviepy import VideoFileClip
class MovieBrowserFrame(wx.Frame):
def __init__(self, parent, title):
super(MovieBrowserFrame, self).__init__(parent, title=title, size=(1000, 700))
# 初始化变量
self.base_folder = "" # 用户选择的根目录
self.file_nodes = {} # 字典,key: 文件完整路径, value: 对应的TreeItemId (用于快速查找)
self.selected_file_path = "" # 当前选中的文件路径
# self.screenshotclass = ScreenshotClass(self.base_folder)
# 创建主面板
panel = wx.Panel(self)
# 创建主布局 (水平分割)
main_sizer = wx.BoxSizer(wx.HORIZONTAL)
panel.SetSizer(main_sizer)
# --- 左侧面板: 目录和文件树 ---
left_panel = wx.Panel(panel)
left_sizer = wx.BoxSizer(wx.VERTICAL)
left_panel.SetSizer(left_sizer)
# 创建一个水平容器放两个按钮
button_sizer = wx.BoxSizer(wx.HORIZONTAL)
# "选择目录" 按钮
self.btn_select_folder = wx.Button(left_panel, label="选择根目录")
self.btn_select_folder.Bind(wx.EVT_BUTTON, self.on_select_folder)
button_sizer.Add(self.btn_select_folder, 0, wx.ALL | wx.EXPAND, 5)
# 新增:"设置预览图" 按钮
self.btn_set_preview_global = wx.Button(left_panel, label="设置预览图")
self.btn_set_preview_global.Bind(wx.EVT_BUTTON, self.on_set_preview_global)
self.btn_set_preview_global.Enable(False) # 初始禁用,直到选择了根目录
button_sizer.Add(self.btn_set_preview_global, 0, wx.ALL, 5)
left_sizer.Add(button_sizer, 0, wx.EXPAND)
# 树形控件 (展示目录和文件)
# self.tree = wx.TreeCtrl(left_panel, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT)
self.tree = wx.TreeCtrl(left_panel, style=wx.TR_DEFAULT_STYLE)
self.tree_root = self.tree.AddRoot("Loading...") # 隐藏根节点,但需要它来添加子节点
self.tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.on_play_file) # 双击事件 (播放)
self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_selection_changed) # 选中项改变事件
left_sizer.Add(self.tree, 1, wx.ALL | wx.EXPAND, 5)
# 将左侧面板加入主布局
main_sizer.Add(left_panel, 1, wx.ALL | wx.EXPAND, 5)
# --- 右侧面板: 预览图和信息 ---
right_panel = wx.Panel(panel)
right_sizer = wx.BoxSizer(wx.VERTICAL)
right_panel.SetSizer(right_sizer)
# 预览图标题
self.lbl_preview_title = wx.StaticText(right_panel, label="电影预览")
self.lbl_preview_title.SetFont(wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
right_sizer.Add(self.lbl_preview_title, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)
# 预览图显示区域
self.bitmap_preview = wx.StaticBitmap(right_panel, wx.ID_ANY, wx.NullBitmap, size=(300, 400))
self.bitmap_preview.SetBackgroundColour(wx.Colour(200, 200, 200)) # 灰色背景
right_sizer.Add(self.bitmap_preview, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 10)
# 电影信息区域
info_sizer = wx.FlexGridSizer(3, 2, 10, 10)
info_sizer.AddGrowableCol(1, 1)
info_sizer.Add(wx.StaticText(right_panel, label="文件名:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.lbl_filename = wx.StaticText(right_panel, label="")
info_sizer.Add(self.lbl_filename, 0, wx.EXPAND)
info_sizer.Add(wx.StaticText(right_panel, label="文件大小:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.lbl_filesize = wx.StaticText(right_panel, label="")
info_sizer.Add(self.lbl_filesize, 0, wx.EXPAND)
info_sizer.Add(wx.StaticText(right_panel, label="所属目录:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.lbl_directory = wx.StaticText(right_panel, label="")
info_sizer.Add(self.lbl_directory, 0, wx.EXPAND)
right_sizer.Add(info_sizer, 0, wx.ALL | wx.EXPAND, 10)
# 播放按钮
self.btn_play = wx.Button(right_panel, label="▶ 播放选中电影")
self.btn_play.Bind(wx.EVT_BUTTON, self.on_play_file_button)
self.btn_play.Enable(False) # 初始禁用
right_sizer.Add(self.btn_play, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)
# 将右侧面板加入主布局
main_sizer.Add(right_panel, 1, wx.ALL | wx.EXPAND, 5)
# 设置最小尺寸
self.SetMinSize((800, 500))
# 初始状态
self.clear_preview()
self.tree.CollapseAll() # 折叠所有
def on_set_preview_global(self, event):
"""全局设置预览图:确认后调用 set_ppp"""
if not self.base_folder or not Path(self.base_folder).is_dir():
wx.MessageBox("请先选择一个有效的根目录。", "提示", wx.OK | wx.ICON_WARNING)
return
# 弹出确认对话框
dlg = wx.MessageDialog(
self,
"确定要重新设置预览图吗?\n此操作将为所有视频生成或更新预览图。",
"确认操作",
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION
)
result = dlg.ShowModal()
dlg.Destroy()
if result == wx.ID_YES:
try:
screenshot = ScreenshotClass(self.base_folder)
video_files = screenshot.video_files
if not video_files:
wx.MessageBox("根目录下未找到任何视频文件。", "提示", wx.OK | wx.ICON_INFORMATION)
return
#创建进度对话框
progress_dlg = wx.ProgressDialog(
"正在生成预览图...",
"准备中...",
maximum=len(video_files),
parent=self,
style=wx.PD_CAN_ABORT | wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME
)
success_count = 0
cancelled = False
for i, video_path in enumerate(video_files):
# 更新进度对话框
msg = f"处理: {os.path.basename(video_path)}"
keep_going, skip = progress_dlg.Update(i, msg)
if not keep_going:
cancelled = True
break
try:
screenshot.screenshot(video_path) # 你的截图逻辑
success_count += 1
except Exception as e:
print(f"处理 {video_path} 时出错: {e}")
# 让界面有机会响应(防止卡死)
wx.Yield()
progress_dlg.Destroy()
if not cancelled:
wx.MessageBox(f"✅ 预览图设置完成!\n成功处理 {success_count}/{len(video_files)} 个视频。", "成功",
wx.OK | wx.ICON_INFORMATION)
else:
wx.MessageBox("操作已取消。", "提示", wx.OK | wx.ICON_INFORMATION)
except Exception as e:
wx.MessageBox(f"设置预览图时出错:\n{str(e)}", "错误", wx.OK | wx.ICON_ERROR)
def on_select_folder(self, event):
dialog = wx.DirDialog(self, "选择电影根目录", style=wx.DD_DEFAULT_STYLE)
if dialog.ShowModal() == wx.ID_OK:
selected_path = dialog.GetPath()
if os.path.isdir(selected_path):
self.base_folder = selected_path
self.build_tree()
self.btn_set_preview_global.Enable(True) # ← 启用新按钮
dialog.Destroy()
def build_tree(self):
"""构建目录和文件的树状结构:一级目录下显示其所有子目录中的视频文件"""
self.tree.DeleteAllItems()
self.file_nodes.clear()
self.selected_file_path = ""
self.clear_preview()
if not self.base_folder or not os.path.exists(self.base_folder):
self.SetTitle("电影浏览器")
return
# 创建根节点
root_name = os.path.basename(self.base_folder)
self.tree_root = self.tree.AddRoot(root_name)
try:
# 获取根目录下的所有条目
entries = os.listdir(self.base_folder)
# 过滤出子目录(一级目录)
subdirs = [entry for entry in entries if os.path.isdir(os.path.join(self.base_folder, entry))]
subdirs.sort()
video_exts = ('.mp4', '.mov', '.avi', '.mkv', '.wmv', '.flv', '.webm', '.m4v')
for subdir_name in subdirs:
subdir_path = os.path.join(self.base_folder, subdir_name)
# 在树中添加一级目录节点
dir_node = self.tree.AppendItem(self.tree_root, subdir_name)
# 🔥 使用 os.walk 递归遍历该一级目录下的所有子目录
video_files_found = [] # 存储 (相对路径, 完整路径) 元组
for current_dirpath, dirnames, filenames in os.walk(subdir_path):
for filename in filenames:
if filename.lower().endswith(video_exts):
file_path = os.path.join(current_dirpath, filename)
# 计算相对于一级目录的路径,用于显示
rel_path = os.path.relpath(current_dirpath, subdir_path)
if rel_path == '.':
display_path = filename
else:
display_path = f"{rel_path} / {filename}" # 显示路径结构
video_files_found.append((display_path, file_path))
# 按显示路径排序
video_files_found.sort(key=lambda x: x[0])
# 将所有找到的视频文件添加为 dir_node 的子节点
for display_path, file_path in video_files_found:
# 使用 display_path 作为节点文本,展示文件路径
file_node = self.tree.AppendItem(dir_node, display_path)
self.file_nodes[file_path] = file_node # 仍用完整路径映射
# 展开根节点
self.tree.Expand(self.tree_root)
self.SetTitle(f"电影浏览器 - {root_name}")
except Exception as e:
wx.MessageBox(f"读取根目录时出错: {str(e)}", "错误", wx.OK | wx.ICON_ERROR)
self.SetTitle("电影浏览器 - 读取错误")
def on_tree_selection_changed(self, event):
"""处理树节点选中改变事件"""
item = event.GetItem()
if not item.IsOk():
return
item_text = self.tree.GetItemText(item)
item_parent = self.tree.GetItemParent(item)
# 只有当选中的是**文件节点**(即其父节点不是隐藏的根节点)时,才更新预览
if item_parent and item_parent != self.tree_root: # 是文件节点
file_path = None
# 在file_nodes字典中查找
for path, node in self.file_nodes.items():
if node == item:
file_path = path
break
if file_path and os.path.exists(file_path):
self.selected_file_path = file_path
self.update_file_info_and_preview(file_path)
self.btn_play.Enable(True)
else:
self.clear_preview()
self.btn_play.Enable(False)
else:
# 选中的是目录节点或根节点
self.clear_preview()
self.btn_play.Enable(False)
def update_file_info_and_preview(self, file_path):
"""更新文件信息和预览图"""
filename = os.path.basename(file_path)
filesize = os.path.getsize(file_path)
# 格式化文件大小
if filesize < 1024:
size_str = f"{filesize} B"
elif filesize < 1024 * 1024:
size_str = f"{filesize / 1024:.1f} KB"
elif filesize < 1024 * 1024 * 1024:
size_str = f"{filesize / (1024*1024):.1f} MB"
else:
size_str = f"{filesize / (1024*1024*1024):.1f} GB"
# 所属目录
dir_name = os.path.basename(os.path.dirname(file_path))
self.lbl_filename.SetLabel(filename)
self.lbl_filesize.SetLabel(size_str)
self.lbl_directory.SetLabel(dir_name)
# 加载预览图
self.load_preview_image(file_path)
def load_preview_image(self, file_path):
"""加载与电影同名的预览图"""
base_name = os.path.splitext(file_path)[0]
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
preview_bitmap = None
for ext in image_extensions:
image_path = base_name + ext
if os.path.exists(image_path):
try:
image = wx.Image(image_path, wx.BITMAP_TYPE_ANY)
img_width, img_height = image.GetSize()
target_width, target_height = 300, 400
scale = min(target_width / img_width, target_height / img_height)
new_width = int(img_width * scale)
new_height = int(img_height * scale)
image = image.Scale(new_width, new_height, wx.IMAGE_QUALITY_HIGH)
preview_bitmap = wx.Bitmap(image)
break
except Exception as e:
print(f"加载图片失败 {image_path}: {e}")
continue
if preview_bitmap:
self.bitmap_preview.SetBitmap(preview_bitmap)
else:
self.bitmap_preview.SetBitmap(wx.NullBitmap)
def clear_preview(self):
"""清空预览区域"""
self.selected_file_path = ""
self.bitmap_preview.SetBitmap(wx.NullBitmap)
self.lbl_filename.SetLabel("")
self.lbl_filesize.SetLabel("")
self.lbl_directory.SetLabel("")
self.btn_play.Enable(False)
def on_play_file(self, event=None):
"""播放选中的文件 (双击或按钮触发)"""
if not self.selected_file_path or not os.path.exists(self.selected_file_path):
wx.MessageBox("请先在列表中选择一个电影文件。", "提示", wx.OK | wx.ICON_INFORMATION)
return
try:
wx.LaunchDefaultApplication(self.selected_file_path)
except Exception as e:
wx.MessageBox(f"无法播放文件: {str(e)}", "播放错误", wx.OK | wx.ICON_ERROR)
def on_play_file_button(self, event):
"""播放按钮点击"""
self.on_play_file()
class MovieBrowserApp(wx.App):
def OnInit(self):
frame = MovieBrowserFrame(None, "电影浏览器")
frame.Show()
return True
class ScreenshotClass:
def __init__(self,path):
self.path = path
self.video_exts = ('.mp4', '.mov', '.avi', '.mkv', '.wmv')
self.video_files = self.count_video()[0]
self.count_video = self.count_video()[1]
def count_video(self):
if Path(self.path).is_dir():
video_files = [f for f in Path(self.path).rglob("*")if f.is_file() and f.suffix.lower() in self.video_exts]
return video_files, len(video_files)
else:
return [self.path],1
def screenshot(self,video_file):
try:
self.snapshot_with_VFC(video_file)
except Exception as e:
print(f"发生错误: {e}")
self.snapshot_with_opencv(video_file)
def snapshot_with_opencv(self,video_path, num_frames=3,):
path = Path(video_path)
print(path)
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise IOError(f"无法打开视频文件: {video_path}")
# 获取总帧数和FPS
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
if total_frames < num_frames:
raise ValueError("视频太短,无法截取足够帧")
# 随机选择帧索引
selected_indices = sorted(random.sample(range(total_frames), num_frames))
images = []
for frame_idx in selected_indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
ret, frame = cap.read()
if not ret:
continue
# BGR → RGB
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = Image.fromarray(frame_rgb)
images.append(img)
cap.release()
# 拼接图像(从上到下)
widths, heights = zip(*(i.size for i in images))
max_width = max(widths)
total_height = sum(heights)
concatenated = Image.new('RGB', (max_width, total_height))
y_offset = 0
for img in images:
concatenated.paste(img, (0, y_offset))
y_offset += img.height
output_path = f"{path.parent/path.stem}.jpg"
# print(output_path)
concatenated.save(output_path)
return output_path
def snapshot_with_VFC(self,video_path):
clip = VideoFileClip(video_path)
# 获取视频时长(秒)
duration = clip.duration
print(duration)
# 在 [0, duration] 范围内随机生成 3 个时间点(浮点数)
times = sorted(np.random.uniform(0, duration, 3))
# 获取截图(帧)
frames = [clip.get_frame(t) for t in times]
# 将 NumPy 数组(帧)转换为 PIL 图像
images = [Image.fromarray(frame) for frame in frames]
# 获取每张图像的尺寸
widths, heights = zip(*(i.size for i in images))
max_width = max(widths)
total_height = sum(heights)
# 创建一个空白图像用于垂直拼接
concatenated_image = Image.new('RGB', (max_width, total_height))
# 从上到下粘贴图像
y_offset = 0
for img in images:
concatenated_image.paste(img, (0, y_offset))
y_offset += img.height
# 保存结果
output_path = f"{video_path.parent/video_path.stem}.jpg"
concatenated_image.save(output_path)
# 释放视频资源
clip.close()
print(f"已保存拼接图像: {output_path}")
print(f"截图时间点: {times}")
if __name__ == '__main__':
app = MovieBrowserApp()
app.MainLoop()