作者: [PASSLINK]
日期: [2024-8-23]
需求分析
- 使用 python 的 tkiner 模块编写一个媒体播放器。
- 该播放器有播放视频 、录像功能。
- 其中播放的视频可以从视频文件夹中挑选,拥有播放、暂停、切换,跳转功能。
- 录像包含开始,暂停,结束,并保存 mp4 格式视频。
设计
左侧为播放视频功能,右侧录像功能。右侧文件夹可以选择目标视频进行播放,左侧视频下方可拖动进度条修改视频播放进度,底部从左到右按钮依次为上一部,播放,暂停,下一步。录像部分在右侧,按钮从左到右依次为播放,暂停,结束保存。
实际效果
编码规范
- 模块名(文件名)使用小写字母与下划线组合,语义清晰。
- 常量名,使用大写、数字、下划线组合。
- 类名:使用双驼峰命名法,单词开头大写。
- 素材文件命名:小写字母、下划线、数字组合,相关资源前缀一致。
- 变量名:小写字母、下划线、数字组合,相关变量前缀一致。
项目结构
│ main.py # 主程序入口
│ requirement.txt # 依赖
│
├─doc # 项目文档
│
├─save_video # 录像文件保存文件夹
│ test.mp4
├─src
│ │ config.py # 常量配置模块
│ │ main_window.py # Tk 程序界面
│ │ video_player.py # 播放对象
│ │ video_record.py # 录像对象
│ │
│ ├─assest # ui资源
│ │
技术
- python 版本 : 3.10.x
- IDE : Pycharm Community
- Tk 主题样式(可选):ttkbootstrap - ttkbootstrap :ttkbootstrap == 1.10.1
- 依赖: opencv-python == 4.10.0.84 pillow == 10.4.0
编码
编码之前,你应当了解 tkiner 模块内容,你可以参考我的笔记:tkinter教程 (yuque.com)
下图为程序原理图:
我们以面向对象思想编程,我们的窗口程序由 MainWindow 对象创建,并将播放功能内置于 VideoPlayer 对象,录制功能内置于 VideoRecord 对象。MainWindow 组装两个功能对象,并通过 Tk 的 Canvas 对象更新视频图像。
编码分为三部:
- 编写 config 模块,定义常用的值,可以在开发过程中按需修改
- 编写 MainWindow 对象,绘制基础 UI 界面,布局所有相关部件。
- 编写 VideoPlayer 对象,将视频图像更新到从MainWindow 获取的 Canvas 对象,负责视频的所有功能。
- 编写 VideoRecord 对象,调用电脑摄像头里编号为 0 的设备,将录制内容实时更新到从MainWindow 获取的 Canvas 对象,负责所有录制功能,并将录制视频编码为 mp4 格式。
配置模块 config
该模块定义了常用文件的路径,以及配置信息。该模块在 MainWindow 中完全引用
# -*- coding: utf-8 -*-
"""
@FileName:config.py
@Description:
@Author:锦沐Python
@Time:2024/8/24 17:27
"""
import os
from pathlib import Path
# 打包使用
# _current_path = Path(".").resolve() / "_internal"
_current_path = Path(".").resolve()
ICON = _current_path / "src" / "assest" / "icon.png"
END_PATH = _current_path / "src" / "assest" / "end.png"
FORWARD_PATH = _current_path / "src" / "assest" / "forward.png"
NEXT_PATH = _current_path / "src" / "assest" / "next.png"
PATH_PATH = _current_path / "src" / "assest" / "path.png"
PAUSE_PATH = _current_path / "src" / "assest" / "pause.png"
PLAY_PATH = _current_path / "src" / "assest" / "play.png"
START_PATH = _current_path / "src" / "assest" / "start.png"
SAVE_VIDEO_PATH = Path(".").resolve() / "save_video"
# 模式
VIDEO_PLAY = 2
VIDEO_RECORD = 4
# 窗口
WIN_SIZE = "900x600+5+5"
WIN_MIN_WIDTH = 700
WIN_MIN_HEUGHT = 500
# 注意,由于Ui图片是白色的,所以使用 黑色主题 才能看清 ui
# 主题 https://ttkbootstrap-zh.readthedocs.io/zh-cn/stable/themes/light/
# solar | darkly | superhero | cyborg | vapor |
THEME_STYLE = "cyborg"
# 按钮风格
BUTTON_STYLE = "Link.TButton"
# 更新速度 ms
UPDATE_TIME = 30
# 检查文件夹是否存在
if not os.path.exists(SAVE_VIDEO_PATH):
# 如果文件夹不存在,则创建它
os.makedirs(SAVE_VIDEO_PATH)
print(f"文件夹 {SAVE_VIDEO_PATH} 已创建。")
else:
print(f"文件夹 {SAVE_VIDEO_PATH} 已存在。")
主窗口 MainWindow
该对象需要绘制UI布局,并将各种部件填充到对应的布局中,布局方式统一使用 pack()。
首先,导入必要的依赖与包:
import tkinter as tk
from tkinter import ttk
from Media.src.config import *
from ttkbootstrap import Style
初始化 MainWindow 属性:
class MainWindow:
"""
绘制 tk 程序 Ui
"""
def __init__(self, title):
# 创建 Tk 窗口
self.window = tk.Tk()
# 设置窗口标题
self.window.title(title)
# 图标
self.icon_img = tk.PhotoImage(file=ICON)
self.window.iconphoto(True, self.icon_img)
# 窗口大小
self.window.geometry(WIN_SIZE)
self.window.minsize(WIN_MIN_WIDTH, WIN_MIN_HEUGHT)
# 设置黑色主题
Style(theme=THEME_STYLE)
# 当前视频进度
self.process_var = tk.IntVar(self.window, value=0)
# 文件保存路径
self.video_save_path = SAVE_VIDEO_PATH
# 当前功能状态
self.mode_state = VIDEO_PLAY
# 初始化布局
self._create_containers()
# 填充部件
self._fill_containers()
启动函数,在 main.py 中引用。
def run_app(self):
"""
启动窗口主循环
"""
self.window.mainloop()
现在我们先把布局规划好,根据设计图,我们把结构分为左右两边,然后依次在左右容器中填充布局和部件。
def _create_containers(self):
# 左边容器
self.left_frame = ttk.Frame(self.window)
self.left_frame.pack(side=tk.LEFT, expand=True, fill=tk.BOTH, ipady=5, ipadx=5)
# 右边容器
self.right_frame = ttk.Frame(self.window, width=200)
self.right_frame.pack(side=tk.RIGHT, fill=tk.Y, ipady=5, ipadx=5)
首先是左容器,左容器包含播放窗口,进度条,控制按钮。其中播放按钮放到了新的容器 control_frame,然后 依次在left_frame 容器填充 video_canvas, process_bar, control_frame。进度条的值为 0 - 100,流动变量值为 self.process_var,
使用 self.process_var.set( value ) 函数可以实时更新进度条。
部件的 command 参数链接的是函数引用,注意只能使用函数名,不要加括号。video_canvas 是画布对象,该参量用于显示图像,所以会作为参数,传递给 VideoPlayer , VideoRecord 对象进行控制。
def _fill_containers(self):
############################# 左边 ########################################
# 视频显示画布
self.video_canvas = tk.Canvas(self.left_frame, borderwidth=4)
self.video_canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.process_bar = ttk.Scale(self.left_frame, from_=0, to=100,
variable=self.process_var, command=self.on_progress_change)
self.process_bar.pack(side=tk.TOP, fill=tk.X, expand=False, padx=5, pady=5)
# 按钮容器
self.control_frame = ttk.Frame(self.left_frame)
self.control_frame.pack(side=tk.BOTTOM, expand=False, fill=tk.X, ipady=5, ipadx=5)
# 保持对 PhotoImage 对象的引用
self.img_forward = tk.PhotoImage(file=FORWARD_PATH)
self.forword_button = ttk.Button(self.control_frame, image=self.img_forward,
style=BUTTON_STYLE, command=self.switch_forward_video)
self.forword_button.pack(side=tk.LEFT, padx=5, expand=True)
self.img_play = tk.PhotoImage(file=PLAY_PATH)
self.img_pause = tk.PhotoImage(file=PAUSE_PATH)
self.play_button = ttk.Button(self.control_frame, image=self.img_play,
style=BUTTON_STYLE, command=self.play_change)
self.play_button.pack(side=tk.LEFT, padx=5, expand=True)
self.img_next = tk.PhotoImage(file=NEXT_PATH)
self.next_button = ttk.Button(self.control_frame, image=self.img_next,
style=BUTTON_STYLE, command=self.switch_next_video)
self.next_button.pack(side=tk.LEFT, padx=5, expand=True)
右容器包含标签文本,录制按钮,打开文件夹按钮,以及列表部件。其中录制按钮放到了新的容器 record_control_frame 内,right_frame 容器依次填充 tip1_label,record_control_frame, open_dir_button, file_list_text。绑定 Treeview 点击事件
使用 self.file_list_text.bind("", self.on_item_click) ,on_item_click 为函数。
################################# 右边 ####################################
# 右边控制栏
self.tip1_label = ttk.Label(self.right_frame, text="录像", anchor=tk.CENTER)
self.tip1_label.pack(fill=tk.X, side=tk.TOP, expand=True)
# 录制控件
self.record_control_frame = ttk.Frame(self.right_frame)
self.record_control_frame.pack(fill=tk.X, expand=True)
self.img_start = tk.PhotoImage(file=START_PATH)
self.img_path = tk.PhotoImage(file=PATH_PATH)
self.record_button = ttk.Button(self.record_control_frame, image=self.img_start,
style=BUTTON_STYLE, command=self.record_change)
self.record_button.pack(side=tk.LEFT, expand=True)
self.img_end = tk.PhotoImage(file=END_PATH)
self.record_end_button = ttk.Button(self.record_control_frame, image=self.img_end,
style=BUTTON_STYLE, command=self.record_end)
self.record_end_button.pack(side=tk.LEFT, expand=True)
self.open_dir_button = ttk.Button(self.right_frame, text="打开文件夹", command=self.open_dir)
self.open_dir_button.pack(fill=tk.X, expand=True)
self.file_list_text = ttk.Treeview(self.right_frame, columns="File", show="headings")
self.file_list_text.heading("File", text="Files")
# 绑定 Treeview 点击事件
self.file_list_text.bind("<ButtonRelease-1>", self.on_item_click)
self.file_list_text.pack(expand=True, fill=tk.Y, pady=5, side=tk.BOTTOM)
被绑定的函数,函数最后再写,我们先完成其他类的编写:
def play_change(self):
"""
切换播放模式
:return:
"""
pass
def on_progress_change(self, value):
"""
变更视频进度
:param value:
"""
pass
def switch_forward_video(self):
"""
切换上一部视频
"""
pass
def switch_next_video(self):
"""
切换下一部视频
"""
pass
def open_dir(self):
"""
打开文件夹对话框,挑选视频文件夹
"""
pass
def on_item_click(self, event):
"""
获取点击事件
:param event:
"""
pass
def record_change(self):
"""
录制状态切换
"""
pass
def record_end(self):
"""
结束录制
"""
pass
视频录制 VideoRecord
视频录制,我们需要先打开摄像头,然后获取每帧图像存储到 mp4 文件。
import cv2
from PIL import Image, ImageTk
from datetime import datetime
# print(cv2.getBuildInformation())
class VideoRecord:
"""
控制本地编号为 0 的摄像机进行录制,
并将画面实时渲染到 canvas 上,
录制结束保存为 mp4 格式
"""
def __init__(self, canvas, save_path):
self.canvas = canvas
# 保存路径
self.save_path = save_path
# 录制中
self.is_recording = False
# 图片对象
self.photo = None
self.vid_camera = None
self.video_writer = None
开始录制功能:先判断 vid_camera 是否为空,不为空代表已经开始录制了,不进行操纵。此判断用于配合 继续录制 功能。
创建新的 VideoCapture 对象,打开编号为 0 的本地相机设备。配置视频解码器,获取视频图像大小,创建 mp4 文件名,然后创建写入对象 VideoWriter。
def start_record(self):
"""
开启摄像头进行采集
XVID : avi
mp4v : mp4
"""
if self.vid_camera:
return
self.vid_camera = cv2.VideoCapture(0, cv2.CAP_DSHOW)
# print(self.vid_camera.isOpened())
self.is_recording = True
if self.save_path:
# 设置视频编解码器和输出文件, 此处编辑器无法识别不用管
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
# 获取图像
# 获取图像大小
frame_size = (int(self.vid_camera.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(self.vid_camera.get(cv2.CAP_PROP_FRAME_HEIGHT)))
file_name = self.save_path / f"record_{datetime.now().strftime('%m-%d_%H-%M-%S')}.mp4"
# 创建写入对象
self.video_writer = cv2.VideoWriter(file_name, fourcc, 30, frame_size)
根据 is_recording 状态决定,暂停与继续。结束操纵,先暂停视频播放,然后关闭摄像头,保存视频文件,最后清空录制对象。
def pause_record(self):
"""
暂停录制
"""
self.is_recording = False
def resume_record(self):
"""
继续录制
"""
self.is_recording = True
def end_record(self):
"""
停止录制
"""
if self.vid_camera:
self.pause_record()
self.vid_camera.release()
if self.video_writer:
self.video_writer.release()
self.vid_camera = None
更新函数,将每一帧图像绘制到画布上,通过 is_recording 状态决定是否更新和录制图像。此函数需要循环更新,我们不能采用多线程模式,多线程模式会导致渲染的图像闪烁。我们采取在 MainWindow 的定时更新函数中执行此函数。
def update(self):
"""
更新页面,保存视频
此处必须先保存后再显示
"""
if self.is_recording:
ret, frame = self.vid_camera.read()
if self.video_writer:
self.video_writer.write(frame)
if ret:
frame = cv2.resize(frame, (self.canvas.winfo_width(), self.canvas.winfo_height()))
self.photo = ImageTk.PhotoImage(image=Image.fromarray(frame))
# 清除画面重新渲染, 锚点要放在左上角
self.canvas.create_image(0, 0, image=self.photo, anchor="nw")
在 MainWindow 中 self._update()
激活 self.window.after(self.update_time, self._update)
函数,形成一个定时闭环,每 UPDATE_TIME 毫秒执行一次 self._update()
函数。于是只要 is_recording 为真,就会快速刷新画布视图,形成流程的视频。
class MainWindow:
"""
绘制 tk 程序 Ui
"""
def __init__(self, title):
# 其他 ...........
# 初始化摄像
self.video_record = None
# 激活画布更新函数, UPDATE_TIME ms 更新一次,不要使用多线程,多线程会导致画面严重闪烁
self.update_time = UPDATE_TIME
self._update()
def _update(self):
if self.video_record and self.mode_state == VIDEO_RECORD:
self.video_record.update()
self.window.after(self.update_time, self._update)
视频播放 VideoPlayer
此类与录制功能很相似,播放视频前,我们要获取视频文件路径,然后根据路径获取视频文件,创建 VideoCapture 对象。使用 current_frame 帧数控制播放进度。
import cv2
from PIL import Image, ImageTk
class VideoPlayer:
"""
播放视频
"""
def __init__(self, canvas, video_source):
self.canvas = canvas
# 图片对象
self.photo = None
# 视频解析对象
self.vid = cv2.VideoCapture(video_source)
# 播放状态
self.is_playing = False
# 当前帧数
self.current_frame = 0
# 总帧数
self.all_frame = int(self.vid.get(cv2.CAP_PROP_FRAME_COUNT))
基础功能:
def play(self):
"""
开始播放
"""
self.is_playing = True
def pause(self):
"""
暂停播放
"""
self.is_playing = False
def resume(self):
"""
继续播放
"""
self.is_playing = True
def close(self):
"""
关闭
"""
self.pause()
self.vid.release()
该函数用于获取实时的播放进度,将在 MianWindow 的更新函数中使用。
def get_current_progress(self):
"""
通过 百分比 计算当前进度
:return:
"""
try:
percent = (self.current_frame / self.all_frame) * 100
return percent
except ZeroDivisionError as e:
print(f"视频打开错误:{e}")
此函数用于手动调整视频进度,更新 self.current_frame 从而改变播放进度。
def set_start(self, percent):
"""
根据百分比,将视频位置设置为指定的帧数
:param percent: 百分比 0 - 1
"""
# print(self.all_frame)
start_time = int(self.all_frame * percent)
print(start_time)
self.vid.set(cv2.CAP_PROP_POS_FRAMES, start_time)
self.current_frame = start_time
更新函数,将每一帧图像绘制到画布上,通过 is_playing状态决定是否更新图像。此函数需要循环更新,我们不能采用多线程模式,多线程模式会导致渲染的图像闪烁。我们采取在 MainWindow 的定时更新函数中执行此函数。
def update(self):
"""
更新页面
"""
if self.is_playing:
ret, frame = self.vid.read()
if ret:
self.current_frame += 1
frame = cv2.resize(frame, (self.canvas.winfo_width(), self.canvas.winfo_height()))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
self.photo = ImageTk.PhotoImage(image=Image.fromarray(frame))
# 只更新图像,不清除整个画布
self.canvas.create_image(0, 0, image=self.photo, anchor="nw")
在 MainWindow 中组装
class MainWindow:
"""
绘制 tk 程序 Ui
"""
def __init__(self, title):
# 其他...............
# 视频路径列表
self.video_source_list = []
# 当前视频索引
self.video_source = ""
# 当前视频进度
self.process_var = tk.IntVar(self.window, value=0)
# 文件保存路径
self.video_save_path = SAVE_VIDEO_PATH
# 当前功能状态
self.mode_state = VIDEO_PLAY
# 初始化播放器
self.video_play = None
# 初始化摄像
self.video_record = None
# 激活画布更新函数, UPDATE_TIME ms 更新一次,不要使用多线程,多线程会导致画面严重闪烁
self.update_time = UPDATE_TIME
self._update()
视图更新
def _update(self):
if self.video_record and self.mode_state == VIDEO_RECORD:
self.video_record.update()
if self.video_play and self.mode_state == VIDEO_PLAY:
self.video_play.update()
# 刷新进度条
self.process_var.set(self.video_play.get_current_progress())
self.window.after(self.update_time, self._update)
关闭函数,用于结束功能时正确处理。
def close_video_player(self):
"""
关闭播放模式
"""
if self.video_play:
self.video_play.close()
self.video_play = None
self.process_var.set(0)
self.play_button.config(image=self.img_play)
def close_video_record(self):
"""
关闭录制模式
"""
if self.video_record:
self.video_record.end_record()
self.video_record = None
self.process_var.set(0)
self.record_button.config(image=self.img_start)
为了方便切换模式,我们可以定义模式切换:
def change_mode(self, mode):
"""
变换模式
:param mode: VIDEO_PLAY | VIDEO_RECORD
"""
if mode == VIDEO_PLAY:
if self.video_source and self.video_source_list:
self.mode_state = mode
self.close_video_record()
else:
self.show_msg(msg="请先打开一个视频文件夹 或 结束录制", code=500)
elif mode == VIDEO_RECORD:
if self.video_save_path:
self.mode_state = mode
self.close_video_player()
else:
self.show_msg(msg="保存路径不存在", code=500)
弹窗,用于提示用户信息。
def show_msg(self, title="提示", msg="什么也没有", code=200):
"""
提示弹窗
:param title: 弹窗标题
:param msg: 内容
:param code: 200 | 300 | 500
"""
if code == 200:
messagebox.showinfo(title=title, message=msg)
elif code == 500:
messagebox.showerror(title=title, message=msg)
elif code == 300:
messagebox.showwarning(title=title, message=msg)
视频播放控制函数
部件 play_button 被点击时激活。暂停,继续播放,video_play 对象不存在拒绝播放。播放时来回切换播放,暂停按钮图案
def play_change(self):
"""
切换播放模式
:return:
"""
if self.video_play is None:
self.show_msg(msg="请先选择视频文件夹", code=500)
return
self.change_mode(mode=VIDEO_PLAY)
# 播放状态切换
if self.video_play.is_playing:
self.play_button.config(image=self.img_play)
self.video_play.pause()
else:
self.video_play.resume()
self.play_button.config(image=self.img_pause)
self.video_play.play()
部件 process_bar 被拖动时激活,用于更改 video_play 对象内部的 当前帧数控制播放进度。
def on_progress_change(self, value):
"""
变更视频进度
:param value:
"""
# print(f"进度条的值改变了: {type(value)}")
if self.video_play:
self.video_play.set_start(float(value) / 100)
部件 forword_button 被点击时激活, 先关闭上一个视频,然后创建新的视频对象,进度归零,播放。
def switch_forward_video(self):
"""
切换上一部视频
"""
self.close_video_player()
if self.video_source_list:
source_index = (self.video_source_list.index(self.video_source) - 1)
% len(self.video_source_list)
self.video_source = self.video_source_list[source_index]
self.video_play = VideoPlayer(self.video_canvas, self.video_source)
self.process_var.set(0)
self.play_change()
else:
self.show_msg(msg="请先选择视频文件夹", code=500)
部件 next_button 被点击时激活。
@mode_wrapper(VIDEO_PLAY)
def switch_next_video(self):
"""
切换下一部视频
"""
self.close_video_player()
if self.video_source_list:
source_index = (self.video_source_list.index(self.video_source) + 1)
% len(self.video_source_list)
self.video_source = self.video_source_list[source_index]
self.video_play = VideoPlayer(self.video_canvas, self.video_source)
self.process_var.set(0)
self.play_change()
else:
self.show_msg(msg="请先选择视频文件夹", code=500)
部件 open_dir_button 被点击时激活,激活弹窗,获取挑选的文件夹,并获取 ".mp4", ".avi" 视频文件,将文件名显示到部件file_list_text 。
def open_dir(self):
"""
打开文件夹对话框,挑选视频文件夹
"""
folder_path = filedialog.askdirectory()
if folder_path:
# 更新 Treeview
self.file_list_text.delete(*self.file_list_text.get_children())
self.video_source_list.clear()
# 获取视频文件列表
video_extensions = (".mp4", ".avi") # 根据需要添加更多扩展名
for file in Path(folder_path).glob("*"):
if file.suffix in video_extensions:
# 路径保存
self.video_source_list.append(str(file))
# 只显示文件名
self.file_list_text.insert("", "end", values=(file.name,))
# 初始化当前播放文件路径
self.video_source = self.video_source_list[0]
self.change_mode(mode=VIDEO_PLAY)
部件 file_list_text 内部被点击时激活,获取被点击对象,根据该对象获取文件下标,创建新的视频对象并播放。
def on_item_click(self, event):
"""
获取点击事件
:param event:
"""
# 获取被点击的列表项
item = self.file_list_text.identify("item", event.x, event.y)
if item:
# 关闭上一个视频
self.close_video_player()
# 获取目标下标
clicked_index = self.file_list_text.index(item)
# 获取目标文件路径
self.video_source = self.video_source_list[clicked_index]
self.video_play = VideoPlayer(self.video_canvas, self.video_source)
self.process_var.set(0)
# 初始化播放器
self.play_change()
视频录制控制函数
部件 record_button 被点击时激活,开始录制或继续录制。
def record_change(self):
"""
录制状态切换
"""
self.change_mode(mode=VIDEO_RECORD)
if self.video_record is None:
# 初始化摄像
self.video_record = VideoRecord(self.video_canvas, self.video_save_path)
if self.video_record.is_recording:
self.record_button.config(image=self.img_start)
self.video_record.pause_record()
else:
self.record_button.config(image=self.img_path)
self.video_record.start_record()
self.video_record.resume_record()
部件 record_end_button 被点击时激活,结束录制。
@mode_wrapper(VIDEO_RECORD)
def record_end(self):
"""
结束录制
"""
self.close_video_record()
模式装饰器
我们不希望使模式之间随意切换,这样会导致程序状态混乱,为了简便,我们可以使用装饰器的特点。该装饰器的功能就是根据 mode 模式判断是否与 MainWindow 的 mode_state 相同,相同就执行被装饰函数。
当然,如果你觉得过于复杂可以选择新增一个模式切换按钮,然后使用该按钮改变模式。
def mode_wrapper(mode):
"""
限制执行函数,只有在模式对应时: mode 与 MainWindow的mode_state相同
才执行被装饰函数
:param mode: VIDEO_RECORD | VIDEO_PLAY
:return:
"""
def decorator(func):
def wrapper(*args, **kwargs):
# 获取类的 self.mode_state 参数
if args[0].mode_state == mode:
return func(*args, **kwargs)
else:
print(f"模式不匹配, 函数:{func.__name__}不执行")
return wrapper
return decorator
经验证,装饰以下函数效果最佳:
@mode_wrapper(VIDEO_PLAY)
def switch_forward_video(self):
"""
切换上一部视频
"""
pass
@mode_wrapper(VIDEO_PLAY)
def switch_next_video(self):
"""
切换下一部视频
"""
pass
@mode_wrapper(VIDEO_PLAY)
def on_item_click(self, event):
"""
获取点击事件
:param event:
"""
pass
@mode_wrapper(VIDEO_RECORD)
def record_end(self):
"""
结束录制
"""
pass
打包exe
配置文件 main.spec
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[ ],
binaries=[],
datas=[
(r"src\assest", r"src\assest"),
],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='视频播放器(无声)',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=[r'src\assest\icon.ico'],
onefile=True
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=["python3.dll"],
name='main',
)
注意,新版本的 pyinstaller 单文件夹模式打包后文件路径会出现错误,因为多了 _internal 文件夹,所以打包时在 config 的_current_path 后添加 / "_internal"
_current_path = Path(".").resolve() / "_internal"
打包命令:
pyinstaller .\main.spec
打包后的结果在 dist 目录下:
项目部署
- 解压源码包 这里要注意,解压源码包后,你应该创建一个新的文件夹,例如 example 。然后把 源码包复制 example 内。你的项目结构应当是这样的。
├─example
│ ├─ Media # 源码包
│ main.py # 主程序入口
│ requirement.txt # 依赖
│ .........
- 创建虚拟环境 venv
用 pycharm 打开 example 文件夹,然后创建一个新的虚拟环境, 注意虚拟环境路径在 example 下,与 Media 同级。
├─example
│ ├─ venv
│ ├─ Media # 源码包
│ main.py # 主程序入口
│ requirement.txt # 依赖
│ .........
- 安装依赖包, 在命令框中执行
pip install -r requirement.txt
- 启动 main.py 即可测试
源码可以在 小红书 搜索:PASSLINK 商店获取