基于Tk的媒体播放器

作者: [PASSLINK]

日期: [2024-8-23]

需求分析

  1. 使用 python 的 tkiner 模块编写一个媒体播放器。
  2. 该播放器有播放视频 、录像功能。
  3. 其中播放的视频可以从视频文件夹中挑选,拥有播放、暂停、切换,跳转功能。
  4. 录像包含开始,暂停,结束,并保存 mp4 格式视频。

设计

左侧为播放视频功能,右侧录像功能。右侧文件夹可以选择目标视频进行播放,左侧视频下方可拖动进度条修改视频播放进度,底部从左到右按钮依次为上一部,播放,暂停,下一步。录像部分在右侧,按钮从左到右依次为播放,暂停,结束保存。

实际效果

编码规范

  1. 模块名(文件名)使用小写字母与下划线组合,语义清晰。
  2. 常量名,使用大写、数字、下划线组合。
  3. 类名:使用双驼峰命名法,单词开头大写。
  4. 素材文件命名:小写字母、下划线、数字组合,相关资源前缀一致。
  5. 变量名:小写字母、下划线、数字组合,相关变量前缀一致。

项目结构

│  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 对象更新视频图像。

编码分为三部:

  1. 编写 config 模块,定义常用的值,可以在开发过程中按需修改
  2. 编写 MainWindow 对象,绘制基础 UI 界面,布局所有相关部件。
  3. 编写 VideoPlayer 对象,将视频图像更新到从MainWindow 获取的 Canvas 对象,负责视频的所有功能。
  4. 编写 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 目录下:

项目部署

  1. 解压源码包 这里要注意,解压源码包后,你应该创建一个新的文件夹,例如 example 。然后把 源码包复制 example 内。你的项目结构应当是这样的。
├─example  
│  ├─ Media # 源码包
│  		main.py   # 主程序入口    
│  		requirement.txt  # 依赖
│		.........
  1. 创建虚拟环境 venv

用 pycharm 打开 example 文件夹,然后创建一个新的虚拟环境, 注意虚拟环境路径在 example 下,与 Media 同级。

├─example  
│  ├─ venv
│  ├─ Media # 源码包
│  		main.py   # 主程序入口    
│  		requirement.txt  # 依赖
│		.........
  1. 安装依赖包, 在命令框中执行
 pip install -r requirement.txt
  1. 启动 main.py 即可测试

源码可以在 小红书 搜索:PASSLINK 商店获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值