Python实现GIF解析与构建系列(六):自定义tk录制工具详解

目录

Python|GIF 解析与构建(6):手搓 tk 录制工具

一、工具功能概览

二、核心架构设计

1. 帧率控制模块

2. 屏幕捕获模块

3. 主应用模块

三、关键技术解析

1. 屏幕捕获技术

2. 帧率控制原理

3. 透明窗口实现

四、使用指南

1. 基本操作

2. 高级技巧

五、优化方向

六、总结


Python|GIF 解析与构建(6):手搓 tk 录制工具

在 GIF 动图的制作流程中,屏幕录制是一个非常实用的功能。通过 Python 的 Tkinter 库,我们可以轻松构建一个轻量级的 GIF 录制工具,实现自定义区域录制、帧率控制等功能。

一、工具功能概览

我们构建的 GIF 录制工具具备以下核心功能:

  • 自定义录制区域:可自由设置录制区域的位置和大小
  • 帧率控制:支持自定义帧率设置,满足不同场景需求
  • 实时坐标显示:显示录制区域在屏幕上的精确坐标
  • 轻量级界面:基于 Tkinter 构建的简洁操作界面
  • 窗口拖动:支持拖动窗口调整位置
  • 这个工具适合用于制作教程演示、软件操作录制等场景,相比专业录制软件更加轻量灵活。

    二、核心架构设计

    工具采用模块化设计,主要包含三个核心类:

    1. 帧率控制模块

    control_frame类负责管理录制帧率,确保录制过程保持稳定的帧速率:

  • 计算每帧的理想间隔时间
  • 监测实际处理时间并进行补偿
  • 统计实际帧率和总录制时间
  • 该模块通过time.sleep()实现精确的时间控制,确保录制的 GIF 流畅无卡顿。

    2. 屏幕捕获模块

    ScreenshotData类封装了屏幕截图功能,基于 Windows API 实现:

  • 使用ctypes调用 GDI32 和 USER32 动态链接库
  • 支持获取屏幕 DPI 并计算缩放比例
  • 通过BitBlt函数实现高效屏幕拷贝
  • 提取像素数据用于后续 GIF 生成
  • 这个模块解决了 Python 中高效屏幕捕获的问题,为 GIF 录制提供了基础数据。

    3. 主应用模块

    GIFALL类是主应用类,负责构建 GUI 界面和协调各模块工作:

  • 构建可视化操作界面,包括参数设置和控制按钮
  • 处理用户交互,如窗口拖动、参数修改
  • 协调屏幕捕获和帧率控制模块完成录制流程
  • 实时更新显示录制区域坐标
  • 三、关键技术解析

    1. 屏幕捕获技术

    在 Windows 环境下实现屏幕捕获,我们采用了 GDI 绘图接口:

    # 通过BitBlt函数拷贝屏幕内容
    SRCCOPY = 0x00CC0020
    self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
    

    这种方法相比 Python 的 PIL 库截图更加高效,能够满足高帧率录制的需求。通过定义 Windows API 中的结构体,我们可以直接获取原始像素数据:

    # 定义RGB颜色结构体
    class RGBQUAD(ctypes.Structure):
        _fields_ = [("rgbBlue", ctypes.c_ubyte),
                    ("rgbGreen", ctypes.c_ubyte),
                    ("rgbRed", ctypes.c_ubyte),
                    ("rgbReserved", ctypes.c_ubyte)]
    

    2. 帧率控制原理

    帧率控制的核心在于计算每帧的理想时间并进行实时补偿:

    def wait(self):
        spend = self.spend()
        true_frame = self.fps_count / (time.time() - self.time_all)
        if true_frame > self.fps:
            if self.time_one_frame - spend > 0:
                time.sleep(self.time_one_frame - spend)
    

    这段代码会计算实际处理一帧所用的时间,并与理想时间比较,通过time.sleep()进行补偿,确保整体帧率稳定。

    3. 透明窗口实现

    为了让录制工具不遮挡屏幕内容,我们实现了透明窗口效果:

    # 设置透明背景色
    self.bg_color = '#FFFFF1'
    self.root.config(bg=self.bg_color)
    self.root.wm_attributes('-transparentcolor', self.bg_color)
    

    通过设置窗口的透明颜色属性,使特定颜色的区域变得透明,提升使用体验。

    四、使用指南

    1. 基本操作

    1. 启动程序后,会看到一个透明的录制窗口
    2. 通过输入框设置录制区域的宽度、高度和坐标
    3. 设置合适的帧率(建议 10-30fps)和总帧数
    4. 点击 "开始录制" 按钮开始录制
    5. 录制完成后会显示总耗时和平均帧率

    2. 高级技巧

  • 拖动窗口可以调整录制区域的位置
  • 实时坐标显示帮助精确定位录制区域
  • 根据录制内容特性调整帧率:
  • 静态内容:10-15fps 即可
  • 动态内容:24-30fps 更流畅
  • 总帧数控制录制时长:时长 = 总帧数 / 帧率
  • 五、优化方向

    当前版本的录制工具还有很多可以改进的地方:

    1. GIF 生成功能:当前只完成了屏幕捕获,需要添加像素数据到 GIF 的转换功能
    2. 文件保存:增加录制结果保存功能,支持自定义文件名和保存路径
    3. 区域选择优化:添加鼠标拖动选择区域的功能,提升操作便捷性
    4. 跨平台支持:当前仅支持 Windows 平台。

    六、总结

    通过这个基于 Tkinter 的 GIF 录制工具,我们深入了解了 Python 在图形界面和系统接口调用方面的能力。从屏幕捕获到帧率控制,再到用户界面设计,每个环节都蕴含着丰富的技术细节。

    代码如下:

    import time
    import ctypes
    import tkinter as tk
    
    
    # 控制帧率
    class control_frame():
        def __init__(self):
            self.start_time = float()  # 每次启动时间
            self.fps = int(10)  # fps
            self.time_one_frame = 1 / self.fps  # 补正时间
            self.fps_count = 0  # 总帧率
            self.time_all = time.time()  # 启动时间
    
        # 启动
        def start(self):
            self.start_time = time.time()
            self.fps_count += 1
    
        # 花销
        def spend(self):
            spend = time.time() - self.start_time
            return spend
    
        # 等待
        def wait(self):
            spend = self.spend()
            true_frame = self.fps_count / (time.time() - self.time_all)
            if true_frame > self.fps:
                if self.time_one_frame - spend > 0:
                    time.sleep(self.time_one_frame - spend)
    
    
    # 获取屏幕数据
    class ScreenshotData():
        def __init__(self):
            self.gdi32 = ctypes.windll.gdi32
            self.user32 = ctypes.windll.user32
    
            # 定义常量
            SM_CXSCREEN = 0
            SM_CYSCREEN = 1
            # 缩放比例
            zoom = 1
            hdc = self.user32.GetDC(None)
            try:
                dpi = self.gdi32.GetDeviceCaps(hdc, 88)
                zoom = dpi / 96.0
            finally:
                self.user32.ReleaseDC(None, hdc)
            self.screenWidth = int(self.user32.GetSystemMetrics(SM_CXSCREEN) * zoom)
            self.screenHeight = int(self.user32.GetSystemMetrics(SM_CYSCREEN) * zoom)
    
        # 屏幕截取
        def capture_screen(self, x, y, width, height):
            # 获取桌面窗口句柄
            hwnd = self.user32.GetDesktopWindow()
            # 获取桌面窗口的设备上下文
            hdc_src = self.user32.GetDC(hwnd)
    
            if len(str(hdc_src)) > 16:
                return 0
    
            # 创建一个与屏幕兼容的内存设备上下文
            hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)
    
            # 创建一个位图
            bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)
    
            # 将位图选入内存设备上下文
            old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)
    
            # 定义SRCCOPY常量
            SRCCOPY = 0x00CC0020
            # 捕获屏幕
            self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
            """
            gdi32.BitBlt(hdc_src,  # 目标设备上下文  
                     x_dest,   # 目标矩形左上角的x坐标  
                     y_dest,   # 目标矩形左上角的y坐标  
                     width,    # 宽度  
                     height,   # 高度  
                     hdc_dest, # 源设备上下文  
                     x_src,    # 源矩形左上角的x坐标(通常是0)  
                     y_src,    # 源矩形左上角的y坐标(通常是0)  
                     SRCCOPY)  # 复制选项
            """
    
            # 定义 RGBQUAD 结构体
            class RGBQUAD(ctypes.Structure):
                _fields_ = [("rgbBlue", ctypes.c_ubyte),
                            ("rgbGreen", ctypes.c_ubyte),
                            ("rgbRed", ctypes.c_ubyte),
                            ("rgbReserved", ctypes.c_ubyte)]
    
            # 定义 BITMAPINFOHEADER 结构体
            class BITMAPINFOHEADER(ctypes.Structure):
                _fields_ = [("biSize", ctypes.c_uint),
                            ("biWidth", ctypes.c_int),
                            ("biHeight", ctypes.c_int),
                            ("biPlanes", ctypes.c_ushort),
                            ("biBitCount", ctypes.c_ushort),
                            ("biCompression", ctypes.c_uint),
                            ("biSizeImage", ctypes.c_uint),
                            ("biXPelsPerMeter", ctypes.c_int),
                            ("biYPelsPerMeter", ctypes.c_int),
                            ("biClrUsed", ctypes.c_uint),
                            ("biClrImportant", ctypes.c_uint)]
    
            # 定义 BITMAPINFO 结构体
            class BITMAPINFO(ctypes.Structure):
                _fields_ = [("bmiHeader", BITMAPINFOHEADER),
                            ("bmiColors", RGBQUAD * 3)]  # 只分配了3个RGBQUAD的空间
    
            BI_RGB = 0
            DIB_RGB_COLORS = 0
    
            # 分配像素数据缓冲区(这里以24位位图为例,每个像素3字节)
            pixel_data = (ctypes.c_ubyte * (width * height * 3))()  # 4
    
            # 填充 BITMAPINFO 结构体
            bmi = BITMAPINFO()
            bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
            bmi.bmiHeader.biWidth = width
            bmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图
            bmi.bmiHeader.biPlanes = 1
            bmi.bmiHeader.biBitCount = 24  # 24即3*8   32
            bmi.bmiHeader.biCompression = BI_RGB  # 无压缩
    
            # 调用 GetDIBits 获取像素数据
            ret = self.gdi32.GetDIBits(hdc_src, bmp, 0, height, pixel_data, ctypes.byref(bmi), DIB_RGB_COLORS)
            if ret == 0:
                print("GetDIBits failed:", ctypes.WinError())
    
            # 恢复设备上下文
            self.gdi32.SelectObject(hdc_dest, old_bmp)
            # 删除内存设备上下文
            self.gdi32.DeleteDC(hdc_dest)
            # 释放桌面窗口的设备上下文
            self.user32.ReleaseDC(hwnd, hdc_src)
            # bmp已经被处理,现在删除它
            self.gdi32.DeleteObject(bmp)
            return pixel_data
    
    
    # GIF录制系统
    class GIFALL():
        def __init__(self, root):
            self.root = root
            self.root.title("gif录制")
            self.root.geometry("500x250")
            self.root.attributes('-topmost', True)  # 设置窗口置顶
            # self.root.overrideredirect(True)# 隐藏标题栏
    
            self.width = 100
            self.height = 100
            self.x_axis = 0
            self.y_axis = 0
            self.fps_choose = 10
            self.frame_total = 100
            self.frame_count = 0
            self.recording = False  # 初始化录制状态
    
            # 左上右下坐标
            self.topleft_x = 0
            self.topleft_y = 0
            self.bottomright_x = 0
            self.bottomright_y = 0
    
            # 设置透明背景色
            self.bg_color = '#FFFFF1'
            self.root.config(bg=self.bg_color)
            self.root.wm_attributes('-transparentcolor', self.bg_color)
    
            # 创建主框架
            self.main_frame = tk.Frame(root, bg='#FFFFF1', bd=0)
            self.main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
    
            # 左侧透明取景区域
            self.left_frame = tk.Frame(self.main_frame, bg='#FFFFF1')
            self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH)
    
            # 右侧控制面板
            self.right_frame = tk.Frame(self.main_frame, bg='#FFFFF1', width=250)
            self.right_frame.pack(side=tk.RIGHT, fill=tk.Y)
            self.right_frame.pack_propagate(False)
    
            # 在左侧区域添加取景框
            self.create_viewfinder()
    
            # 添加右侧控制面板内容
            self.create_control_panel()
    
            # 添加窗口拖动功能
            self.root.bind("<ButtonPress-1>", self.start_move)
            self.root.bind("<ButtonRelease-1>", self.stop_move)
            self.root.bind("<B1-Motion>", self.on_move)
    
            # 启动坐标更新循环
            self.update_coordinates()
    
        # 录制栏
        def create_viewfinder(self):
            # 创建取景框
            canvas_width = self.width + self.x_axis + 2
            canvas_height = self.height + self.y_axis + 2
            self.canvas = tk.Canvas(
                self.left_frame,
                bg="#FFFFF1",
                width=canvas_width,
                height=canvas_height,
                highlightthickness=0
            )
            self.canvas.pack(padx=0, pady=0)
    
            # 绘制取景框
            self.viewfinder = self.canvas.create_rectangle(
                self.x_axis, self.y_axis, self.x_axis + self.width + 2, self.y_axis + self.height + 2,
                outline="#00BFFF",
                width=2,
                dash=(5, 20)
            )
    
        # 操作栏
        def create_control_panel(self):
            # 尺寸信息
            size_frame = tk.Frame(self.right_frame, bg=self.bg_color)
            size_frame.pack(pady=0, padx=5, fill=tk.X)
            self.width_vr = tk.StringVar(value=str(self.width))
            self.height_vr = tk.StringVar(value=str(self.height))
            self.x_axis_vr = tk.StringVar(value=str(self.x_axis))
            self.y_axis_vr = tk.StringVar(value=str(self.y_axis))
            self.fps_vr = tk.StringVar(value=str(self.fps_choose))
            self.frame_vr = tk.StringVar(value=str(self.frame_total))
    
            # 绑定变量变化事件
            self.width_vr.trace_add("write", self.on_dimension_change)
            self.height_vr.trace_add("write", self.on_dimension_change)
            self.x_axis_vr.trace_add("write", self.on_dimension_change)
            self.y_axis_vr.trace_add("write", self.on_dimension_change)
            self.fps_vr.trace_add("write", self.on_fps_change)  # 绑定帧率变化事件
            self.frame_vr.trace_add("write", self.on_fps_change)  # 绑定帧率变化事件
    
            # 创建宽度输入框
            tk.Label(size_frame, text="宽度:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.width_vr, width=5).grid(row=0, column=1, padx=5, pady=5)
    
            # 创建高度输入框
            tk.Label(size_frame, text="高度:").grid(row=0, column=3, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.height_vr, width=5).grid(row=0, column=4, padx=5, pady=5)
    
            # 创建s轴输入框
            tk.Label(size_frame, text="x轴:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.x_axis_vr, width=5).grid(row=1, column=1, padx=5, pady=5)
    
            # 创建y轴输入框
            tk.Label(size_frame, text="y轴:").grid(row=1, column=3, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.y_axis_vr, width=5).grid(row=1, column=4, padx=5, pady=5)
    
            # 创建帧率输入框
            tk.Label(size_frame, text="帧率:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.fps_vr, width=5).grid(row=2, column=1, padx=5, pady=5)
    
            # 创建总帧率输入框
            tk.Label(size_frame, text="总帧率:").grid(row=2, column=3, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.frame_vr, width=5).grid(row=2, column=4, padx=5, pady=5)
    
            # 添加坐标显示标签
            self.coord_frame = tk.Frame(self.right_frame, bg=self.bg_color)
            self.coord_frame.pack(pady=5)
            self.topleft_label = tk.Label(
                self.coord_frame,
                text="(0, 0)",
                bg=self.bg_color,
                fg="#FFFFFF",
                font=("Arial", 10)
            )
            self.topleft_label.grid(row=0, column=0)
    
            self.bottomright_label = tk.Label(
                self.coord_frame,
                text="(0, 0)",
                bg=self.bg_color,
                fg="#FFFFFF",
                font=("Arial", 10)
            )
            self.bottomright_label.grid(row=0, column=1)
    
            # 控制按钮
            button_frame = tk.Frame(self.right_frame, bg=self.bg_color)
            button_frame.pack(pady=10, padx=5, fill=tk.X)
    
            self.record_btn = tk.Button(
                button_frame,
                text="开始录制",
                command=self.toggle_recording,
                bg="#E74C3C",
                fg="white",
                font=("Arial", 12, "bold"),
                relief="flat",
                padx=20,
                pady=10,
                width=15
            )
            self.record_btn.pack(pady=10)
    
            tk.Button(
                button_frame,
                text="退出应用",
                command=self.root.destroy,
                bg="#000011",
                fg="white",
                font=("Arial", 12),
                relief="flat",
                padx=0,
                pady=0
            ).pack(pady=5)
    
        # 更新尺寸
        def on_dimension_change(self, *args):
            """当尺寸输入框内容变化时更新取景框尺寸"""
            try:
                # 获取新的尺寸值
                new_width = int(self.width_vr.get())
                new_height = int(self.height_vr.get())
                new_x_axis = int(self.x_axis_vr.get())
                new_y_axis = int(self.y_axis_vr.get())
    
                # 验证尺寸有效性
                if new_width > 0 and new_height > 0:
                    # 锁定
                    if new_width > 500:
                        new_width = 500
                    if new_height > 500:
                        new_height = 500
                    # 更新类属性
                    self.width = new_width
                    self.height = new_height
                    # 锁定
                    if new_x_axis > 500:
                        new_x_axis = 500
                    if new_y_axis > 500:
                        new_y_axis = 500
                    if new_x_axis == "":
                        new_x_axis = 0
                    # 更新类属性
                    self.x_axis = new_x_axis
                    self.y_axis = new_y_axis
                    # 更新取景框
                    self.update_viewfinder()
                    # 更新坐标显示
                    self.update_coordinates()
    
            except ValueError:
                # 输入非数字时忽略
                pass
    
        # 更新重新绘制
        def update_viewfinder(self):
            """更新取景框尺寸"""
            # 重新配置Canvas大小
            self.canvas.config(width=self.width + self.x_axis + 2, height=self.height + self.y_axis + 2)
    
            # 更新取景框矩形
            self.canvas.coords(self.viewfinder, self.x_axis, self.y_axis, self.width + self.x_axis + 2,
                               self.height + self.y_axis + 2)
    
            # 强制刷新Canvas
            self.canvas.update_idletasks()
    
        # 更新坐标
        def update_coordinates(self):
            """更新取景框的坐标显示"""
            titlebar_height = 30
            border_width = 1
            correction_value = titlebar_height + border_width
            correction_left_value = 8
    
            # 获取窗口在屏幕上的位置
            window_x = self.root.winfo_x() + correction_left_value
            window_y = self.root.winfo_y() + correction_value
    
            # 计算取景框在屏幕上的绝对坐标
            self.topleft_x = window_x + self.x_axis + 1
            self.topleft_y = window_y + self.y_axis + 1
            self.bottomright_x = self.topleft_x + self.width - 1
            self.bottomright_y = self.topleft_y + self.height - 1
    
            # 更新坐标标签
            self.topleft_label.config(text=f"({self.topleft_x},{self.topleft_y})")
            self.bottomright_label.config(text=f"({self.bottomright_x},{self.bottomright_y})")
    
            # 每秒更新一次坐标
            self.root.after(1000, self.update_coordinates)
    
        # 更新显示
        def on_fps_change(self, *args):
            """当帧率输入框内容变化时更新显示"""
            try:
                new_fps = int(self.fps_vr.get())
                new_frame = int(self.frame_vr.get())
                # 锁定
                if new_fps < 1:
                    new_fps = 1
                elif new_fps > 100:
                    new_fps = 100
                self.fps_choose = new_fps
                if new_frame < 1:
                    new_frame = 1
                self.frame_total = new_frame
            except ValueError:
                # 输入非数字时忽略
                pass
    
        # 录制
        def toggle_recording(self):
            if not self.recording:
                # 开始录制
                self.recording = True
                self.record_btn.config(text="停止录制", bg="#2ECC71")
                Screenshot = ScreenshotData()
                wait = control_frame()
                # 帧率设置
                wait.fps = self.fps_choose
                self.st = time.time()
                def work():
                    wait.start()
                    data = Screenshot.capture_screen(self.topleft_x, self.topleft_y, self.bottomright_x-self.topleft_x+1, self.bottomright_y-self.topleft_y+1)
                    wait.wait()
                    # print(self.topleft_x, self.topleft_y, self.bottomright_x-self.topleft_x+1, self.bottomright_y-self.topleft_y+1)
                    self.frame_count+=1
                    if self.frame_count == self.frame_total:
                        self.frame_count = 0
                        self.recording = False
                        self.record_btn.config(text="开始录制", bg="#E74C3C")
                        print("耗费时间:",time.time()-self.st)
                        print("秒平均帧:",self.frame_total/(time.time()-self.st))
                        return True
                    self.root.after(1, work)
                self.root.after(1,work)
    
            else:
                # 停止录制
                self.recording = False
                self.record_btn.config(text="开始录制", bg="#E74C3C")
    
        # 窗口拖动功能
        def start_move(self, event):
            self.x = event.x
            self.y = event.y
    
        def stop_move(self, event):
            self.x = None
            self.y = None
    
        def on_move(self, event):
            deltax = event.x - self.x
            deltay = event.y - self.y
            x = self.root.winfo_x() + deltax
            y = self.root.winfo_y() + deltay
            self.root.geometry(f"+{x}+{y}")
            # 窗口移动后更新坐标
            self.update_coordinates()
    
    
    if __name__ == '__main__':
        root = tk.Tk()
        app = GIFALL(root)
        root.mainloop()
    

    import time
    import ctypes
    import tkinter as tk
    
    
    # 控制帧率
    class control_frame():
        def __init__(self):
            self.start_time = float()  # 每次启动时间
            self.fps = int(10)  # fps
            self.time_one_frame = 1 / self.fps  # 补正时间
            self.fps_count = 0  # 总帧率
            self.time_all = time.time()  # 启动时间
    
        # 启动
        def start(self):
            self.start_time = time.time()
            self.fps_count += 1
    
        # 花销
        def spend(self):
            spend = time.time() - self.start_time
            return spend
    
        # 等待
        def wait(self):
            spend = self.spend()
            true_frame = self.fps_count / (time.time() - self.time_all)
            if true_frame > self.fps:
                if self.time_one_frame - spend > 0:
                    time.sleep(self.time_one_frame - spend)
    
    
    # 获取屏幕数据
    class ScreenshotData():
        def __init__(self):
            self.gdi32 = ctypes.windll.gdi32
            self.user32 = ctypes.windll.user32
    
            # 定义常量
            SM_CXSCREEN = 0
            SM_CYSCREEN = 1
            # 缩放比例
            zoom = 1
            hdc = self.user32.GetDC(None)
            try:
                dpi = self.gdi32.GetDeviceCaps(hdc, 88)
                zoom = dpi / 96.0
            finally:
                self.user32.ReleaseDC(None, hdc)
            self.screenWidth = int(self.user32.GetSystemMetrics(SM_CXSCREEN) * zoom)
            self.screenHeight = int(self.user32.GetSystemMetrics(SM_CYSCREEN) * zoom)
    
        # 屏幕截取
        def capture_screen(self, x, y, width, height):
            # 获取桌面窗口句柄
            hwnd = self.user32.GetDesktopWindow()
            # 获取桌面窗口的设备上下文
            hdc_src = self.user32.GetDC(hwnd)
    
            if len(str(hdc_src)) > 16:
                return 0
    
            # 创建一个与屏幕兼容的内存设备上下文
            hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)
    
            # 创建一个位图
            bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)
    
            # 将位图选入内存设备上下文
            old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)
    
            # 定义SRCCOPY常量
            SRCCOPY = 0x00CC0020
            # 捕获屏幕
            self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
            """
            gdi32.BitBlt(hdc_src,  # 目标设备上下文  
                     x_dest,   # 目标矩形左上角的x坐标  
                     y_dest,   # 目标矩形左上角的y坐标  
                     width,    # 宽度  
                     height,   # 高度  
                     hdc_dest, # 源设备上下文  
                     x_src,    # 源矩形左上角的x坐标(通常是0)  
                     y_src,    # 源矩形左上角的y坐标(通常是0)  
                     SRCCOPY)  # 复制选项
            """
    
            # 定义 RGBQUAD 结构体
            class RGBQUAD(ctypes.Structure):
                _fields_ = [("rgbBlue", ctypes.c_ubyte),
                            ("rgbGreen", ctypes.c_ubyte),
                            ("rgbRed", ctypes.c_ubyte),
                            ("rgbReserved", ctypes.c_ubyte)]
    
            # 定义 BITMAPINFOHEADER 结构体
            class BITMAPINFOHEADER(ctypes.Structure):
                _fields_ = [("biSize", ctypes.c_uint),
                            ("biWidth", ctypes.c_int),
                            ("biHeight", ctypes.c_int),
                            ("biPlanes", ctypes.c_ushort),
                            ("biBitCount", ctypes.c_ushort),
                            ("biCompression", ctypes.c_uint),
                            ("biSizeImage", ctypes.c_uint),
                            ("biXPelsPerMeter", ctypes.c_int),
                            ("biYPelsPerMeter", ctypes.c_int),
                            ("biClrUsed", ctypes.c_uint),
                            ("biClrImportant", ctypes.c_uint)]
    
            # 定义 BITMAPINFO 结构体
            class BITMAPINFO(ctypes.Structure):
                _fields_ = [("bmiHeader", BITMAPINFOHEADER),
                            ("bmiColors", RGBQUAD * 3)]  # 只分配了3个RGBQUAD的空间
    
            BI_RGB = 0
            DIB_RGB_COLORS = 0
    
            # 分配像素数据缓冲区(这里以24位位图为例,每个像素3字节)
            pixel_data = (ctypes.c_ubyte * (width * height * 3))()  # 4
    
            # 填充 BITMAPINFO 结构体
            bmi = BITMAPINFO()
            bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
            bmi.bmiHeader.biWidth = width
            bmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图
            bmi.bmiHeader.biPlanes = 1
            bmi.bmiHeader.biBitCount = 24  # 24即3*8   32
            bmi.bmiHeader.biCompression = BI_RGB  # 无压缩
    
            # 调用 GetDIBits 获取像素数据
            ret = self.gdi32.GetDIBits(hdc_src, bmp, 0, height, pixel_data, ctypes.byref(bmi), DIB_RGB_COLORS)
            if ret == 0:
                print("GetDIBits failed:", ctypes.WinError())
    
            # 恢复设备上下文
            self.gdi32.SelectObject(hdc_dest, old_bmp)
            # 删除内存设备上下文
            self.gdi32.DeleteDC(hdc_dest)
            # 释放桌面窗口的设备上下文
            self.user32.ReleaseDC(hwnd, hdc_src)
            # bmp已经被处理,现在删除它
            self.gdi32.DeleteObject(bmp)
            return pixel_data
    
    
    # GIF录制系统
    class GIFALL():
        def __init__(self, root):
            self.root = root
            self.root.title("gif录制")
            self.root.geometry("500x250")
            self.root.attributes('-topmost', True)  # 设置窗口置顶
            # self.root.overrideredirect(True)# 隐藏标题栏
    
            self.width = 100
            self.height = 100
            self.x_axis = 0
            self.y_axis = 0
            self.fps_choose = 10
            self.frame_total = 100
            self.frame_count = 0
            self.recording = False  # 初始化录制状态
    
            # 左上右下坐标
            self.topleft_x = 0
            self.topleft_y = 0
            self.bottomright_x = 0
            self.bottomright_y = 0
    
            # 设置透明背景色
            self.bg_color = '#FFFFF1'
            self.root.config(bg=self.bg_color)
            self.root.wm_attributes('-transparentcolor', self.bg_color)
    
            # 创建主框架
            self.main_frame = tk.Frame(root, bg='#FFFFF1', bd=0)
            self.main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
    
            # 左侧透明取景区域
            self.left_frame = tk.Frame(self.main_frame, bg='#FFFFF1')
            self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH)
    
            # 右侧控制面板
            self.right_frame = tk.Frame(self.main_frame, bg='#FFFFF1', width=250)
            self.right_frame.pack(side=tk.RIGHT, fill=tk.Y)
            self.right_frame.pack_propagate(False)
    
            # 在左侧区域添加取景框
            self.create_viewfinder()
    
            # 添加右侧控制面板内容
            self.create_control_panel()
    
            # 添加窗口拖动功能
            self.root.bind("<ButtonPress-1>", self.start_move)
            self.root.bind("<ButtonRelease-1>", self.stop_move)
            self.root.bind("<B1-Motion>", self.on_move)
    
            # 启动坐标更新循环
            self.update_coordinates()
    
        # 录制栏
        def create_viewfinder(self):
            # 创建取景框
            canvas_width = self.width + self.x_axis + 2
            canvas_height = self.height + self.y_axis + 2
            self.canvas = tk.Canvas(
                self.left_frame,
                bg="#FFFFF1",
                width=canvas_width,
                height=canvas_height,
                highlightthickness=0
            )
            self.canvas.pack(padx=0, pady=0)
    
            # 绘制取景框
            self.viewfinder = self.canvas.create_rectangle(
                self.x_axis, self.y_axis, self.x_axis + self.width + 2, self.y_axis + self.height + 2,
                outline="#00BFFF",
                width=2,
                dash=(5, 20)
            )
    
        # 操作栏
        def create_control_panel(self):
            # 尺寸信息
            size_frame = tk.Frame(self.right_frame, bg=self.bg_color)
            size_frame.pack(pady=0, padx=5, fill=tk.X)
            self.width_vr = tk.StringVar(value=str(self.width))
            self.height_vr = tk.StringVar(value=str(self.height))
            self.x_axis_vr = tk.StringVar(value=str(self.x_axis))
            self.y_axis_vr = tk.StringVar(value=str(self.y_axis))
            self.fps_vr = tk.StringVar(value=str(self.fps_choose))
            self.frame_vr = tk.StringVar(value=str(self.frame_total))
    
            # 绑定变量变化事件
            self.width_vr.trace_add("write", self.on_dimension_change)
            self.height_vr.trace_add("write", self.on_dimension_change)
            self.x_axis_vr.trace_add("write", self.on_dimension_change)
            self.y_axis_vr.trace_add("write", self.on_dimension_change)
            self.fps_vr.trace_add("write", self.on_fps_change)  # 绑定帧率变化事件
            self.frame_vr.trace_add("write", self.on_fps_change)  # 绑定帧率变化事件
    
            # 创建宽度输入框
            tk.Label(size_frame, text="宽度:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.width_vr, width=5).grid(row=0, column=1, padx=5, pady=5)
    
            # 创建高度输入框
            tk.Label(size_frame, text="高度:").grid(row=0, column=3, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.height_vr, width=5).grid(row=0, column=4, padx=5, pady=5)
    
            # 创建s轴输入框
            tk.Label(size_frame, text="x轴:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.x_axis_vr, width=5).grid(row=1, column=1, padx=5, pady=5)
    
            # 创建y轴输入框
            tk.Label(size_frame, text="y轴:").grid(row=1, column=3, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.y_axis_vr, width=5).grid(row=1, column=4, padx=5, pady=5)
    
            # 创建帧率输入框
            tk.Label(size_frame, text="帧率:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.fps_vr, width=5).grid(row=2, column=1, padx=5, pady=5)
    
            # 创建总帧率输入框
            tk.Label(size_frame, text="总帧率:").grid(row=2, column=3, padx=5, pady=5, sticky="w")
            tk.Entry(size_frame, textvariable=self.frame_vr, width=5).grid(row=2, column=4, padx=5, pady=5)
    
            # 添加坐标显示标签
            self.coord_frame = tk.Frame(self.right_frame, bg=self.bg_color)
            self.coord_frame.pack(pady=5)
            self.topleft_label = tk.Label(
                self.coord_frame,
                text="(0, 0)",
                bg=self.bg_color,
                fg="#FFFFFF",
                font=("Arial", 10)
            )
            self.topleft_label.grid(row=0, column=0)
    
            self.bottomright_label = tk.Label(
                self.coord_frame,
                text="(0, 0)",
                bg=self.bg_color,
                fg="#FFFFFF",
                font=("Arial", 10)
            )
            self.bottomright_label.grid(row=0, column=1)
    
            # 控制按钮
            button_frame = tk.Frame(self.right_frame, bg=self.bg_color)
            button_frame.pack(pady=10, padx=5, fill=tk.X)
    
            self.record_btn = tk.Button(
                button_frame,
                text="开始录制",
                command=self.toggle_recording,
                bg="#E74C3C",
                fg="white",
                font=("Arial", 12, "bold"),
                relief="flat",
                padx=20,
                pady=10,
                width=15
            )
            self.record_btn.pack(pady=10)
    
            tk.Button(
                button_frame,
                text="退出应用",
                command=self.root.destroy,
                bg="#000011",
                fg="white",
                font=("Arial", 12),
                relief="flat",
                padx=0,
                pady=0
            ).pack(pady=5)
    
        # 更新尺寸
        def on_dimension_change(self, *args):
            """当尺寸输入框内容变化时更新取景框尺寸"""
            try:
                # 获取新的尺寸值
                new_width = int(self.width_vr.get())
                new_height = int(self.height_vr.get())
                new_x_axis = int(self.x_axis_vr.get())
                new_y_axis = int(self.y_axis_vr.get())
    
                # 验证尺寸有效性
                if new_width > 0 and new_height > 0:
                    # 锁定
                    if new_width > 500:
                        new_width = 500
                    if new_height > 500:
                        new_height = 500
                    # 更新类属性
                    self.width = new_width
                    self.height = new_height
                    # 锁定
                    if new_x_axis > 500:
                        new_x_axis = 500
                    if new_y_axis > 500:
                        new_y_axis = 500
                    if new_x_axis == "":
                        new_x_axis = 0
                    # 更新类属性
                    self.x_axis = new_x_axis
                    self.y_axis = new_y_axis
                    # 更新取景框
                    self.update_viewfinder()
                    # 更新坐标显示
                    self.update_coordinates()
    
            except ValueError:
                # 输入非数字时忽略
                pass
    
        # 更新重新绘制
        def update_viewfinder(self):
            """更新取景框尺寸"""
            # 重新配置Canvas大小
            self.canvas.config(width=self.width + self.x_axis + 2, height=self.height + self.y_axis + 2)
    
            # 更新取景框矩形
            self.canvas.coords(self.viewfinder, self.x_axis, self.y_axis, self.width + self.x_axis + 2,
                               self.height + self.y_axis + 2)
    
            # 强制刷新Canvas
            self.canvas.update_idletasks()
    
        # 更新坐标
        def update_coordinates(self):
            """更新取景框的坐标显示"""
            titlebar_height = 30
            border_width = 1
            correction_value = titlebar_height + border_width
            correction_left_value = 8
    
            # 获取窗口在屏幕上的位置
            window_x = self.root.winfo_x() + correction_left_value
            window_y = self.root.winfo_y() + correction_value
    
            # 计算取景框在屏幕上的绝对坐标
            self.topleft_x = window_x + self.x_axis + 1
            self.topleft_y = window_y + self.y_axis + 1
            self.bottomright_x = self.topleft_x + self.width - 1
            self.bottomright_y = self.topleft_y + self.height - 1
    
            # 更新坐标标签
            self.topleft_label.config(text=f"({self.topleft_x},{self.topleft_y})")
            self.bottomright_label.config(text=f"({self.bottomright_x},{self.bottomright_y})")
    
            # 每秒更新一次坐标
            self.root.after(1000, self.update_coordinates)
    
        # 更新显示
        def on_fps_change(self, *args):
            """当帧率输入框内容变化时更新显示"""
            try:
                new_fps = int(self.fps_vr.get())
                new_frame = int(self.frame_vr.get())
                # 锁定
                if new_fps < 1:
                    new_fps = 1
                elif new_fps > 100:
                    new_fps = 100
                self.fps_choose = new_fps
                if new_frame < 1:
                    new_frame = 1
                self.frame_total = new_frame
            except ValueError:
                # 输入非数字时忽略
                pass
    
        # 录制
        def toggle_recording(self):
            if not self.recording:
                # 开始录制
                self.recording = True
                self.record_btn.config(text="停止录制", bg="#2ECC71")
                Screenshot = ScreenshotData()
                wait = control_frame()
                # 帧率设置
                wait.fps = self.fps_choose
                self.st = time.time()
                def work():
                    wait.start()
                    data = Screenshot.capture_screen(self.topleft_x, self.topleft_y, self.bottomright_x-self.topleft_x+1, self.bottomright_y-self.topleft_y+1)
                    wait.wait()
                    # print(self.topleft_x, self.topleft_y, self.bottomright_x-self.topleft_x+1, self.bottomright_y-self.topleft_y+1)
                    self.frame_count+=1
                    if self.frame_count == self.frame_total:
                        self.frame_count = 0
                        self.recording = False
                        self.record_btn.config(text="开始录制", bg="#E74C3C")
                        print("耗费时间:",time.time()-self.st)
                        print("秒平均帧:",self.frame_total/(time.time()-self.st))
                        return True
                    self.root.after(1, work)
                self.root.after(1,work)
    
            else:
                # 停止录制
                self.recording = False
                self.record_btn.config(text="开始录制", bg="#E74C3C")
    
        # 窗口拖动功能
        def start_move(self, event):
            self.x = event.x
            self.y = event.y
    
        def stop_move(self, event):
            self.x = None
            self.y = None
    
        def on_move(self, event):
            deltax = event.x - self.x
            deltay = event.y - self.y
            x = self.root.winfo_x() + deltax
            y = self.root.winfo_y() + deltay
            self.root.geometry(f"+{x}+{y}")
            # 窗口移动后更新坐标
            self.update_coordinates()
    
    
    if __name__ == '__main__':
        root = tk.Tk()
        app = GIFALL(root)
        root.mainloop()
    

    作者:myzzb

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python实现GIF解析与构建系列(六):自定义tk录制工具详解

    发表回复