当前位置: 首页 > news >正文

Python|GIF 解析与构建(6):手搓 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)  # fpsself.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_timereturn 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.gdi32self.user32 = ctypes.windll.user32# 定义常量SM_CXSCREEN = 0SM_CYSCREEN = 1# 缩放比例zoom = 1hdc = self.user32.GetDC(None)try:dpi = self.gdi32.GetDeviceCaps(hdc, 88)zoom = dpi / 96.0finally: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 = 0DIB_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 = widthbmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图bmi.bmiHeader.biPlanes = 1bmi.bmiHeader.biBitCount = 24  # 24即3*8   32bmi.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 = rootself.root.title("gif录制")self.root.geometry("500x250")self.root.attributes('-topmost', True)  # 设置窗口置顶# self.root.overrideredirect(True)# 隐藏标题栏self.width = 100self.height = 100self.x_axis = 0self.y_axis = 0self.fps_choose = 10self.frame_total = 100self.frame_count = 0self.recording = False  # 初始化录制状态# 左上右下坐标self.topleft_x = 0self.topleft_y = 0self.bottomright_x = 0self.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 + 2canvas_height = self.height + self.y_axis + 2self.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 = 500if new_height > 500:new_height = 500# 更新类属性self.width = new_widthself.height = new_height# 锁定if new_x_axis > 500:new_x_axis = 500if new_y_axis > 500:new_y_axis = 500if new_x_axis == "":new_x_axis = 0# 更新类属性self.x_axis = new_x_axisself.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)# 强制刷新Canvasself.canvas.update_idletasks()# 更新坐标def update_coordinates(self):"""更新取景框的坐标显示"""titlebar_height = 30border_width = 1correction_value = titlebar_height + border_widthcorrection_left_value = 8# 获取窗口在屏幕上的位置window_x = self.root.winfo_x() + correction_left_valuewindow_y = self.root.winfo_y() + correction_value# 计算取景框在屏幕上的绝对坐标self.topleft_x = window_x + self.x_axis + 1self.topleft_y = window_y + self.y_axis + 1self.bottomright_x = self.topleft_x + self.width - 1self.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 = 1elif new_fps > 100:new_fps = 100self.fps_choose = new_fpsif new_frame < 1:new_frame = 1self.frame_total = new_frameexcept ValueError:# 输入非数字时忽略pass# 录制def toggle_recording(self):if not self.recording:# 开始录制self.recording = Trueself.record_btn.config(text="停止录制", bg="#2ECC71")Screenshot = ScreenshotData()wait = control_frame()# 帧率设置wait.fps = self.fps_chooseself.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+=1if self.frame_count == self.frame_total:self.frame_count = 0self.recording = Falseself.record_btn.config(text="开始录制", bg="#E74C3C")print("耗费时间:",time.time()-self.st)print("秒平均帧:",self.frame_total/(time.time()-self.st))return Trueself.root.after(1, work)self.root.after(1,work)else:# 停止录制self.recording = Falseself.record_btn.config(text="开始录制", bg="#E74C3C")# 窗口拖动功能def start_move(self, event):self.x = event.xself.y = event.ydef stop_move(self, event):self.x = Noneself.y = Nonedef on_move(self, event):deltax = event.x - self.xdeltay = event.y - self.yx = self.root.winfo_x() + deltaxy = self.root.winfo_y() + deltayself.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)  # fpsself.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_timereturn 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.gdi32self.user32 = ctypes.windll.user32# 定义常量SM_CXSCREEN = 0SM_CYSCREEN = 1# 缩放比例zoom = 1hdc = self.user32.GetDC(None)try:dpi = self.gdi32.GetDeviceCaps(hdc, 88)zoom = dpi / 96.0finally: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 = 0DIB_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 = widthbmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图bmi.bmiHeader.biPlanes = 1bmi.bmiHeader.biBitCount = 24  # 24即3*8   32bmi.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 = rootself.root.title("gif录制")self.root.geometry("500x250")self.root.attributes('-topmost', True)  # 设置窗口置顶# self.root.overrideredirect(True)# 隐藏标题栏self.width = 100self.height = 100self.x_axis = 0self.y_axis = 0self.fps_choose = 10self.frame_total = 100self.frame_count = 0self.recording = False  # 初始化录制状态# 左上右下坐标self.topleft_x = 0self.topleft_y = 0self.bottomright_x = 0self.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 + 2canvas_height = self.height + self.y_axis + 2self.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 = 500if new_height > 500:new_height = 500# 更新类属性self.width = new_widthself.height = new_height# 锁定if new_x_axis > 500:new_x_axis = 500if new_y_axis > 500:new_y_axis = 500if new_x_axis == "":new_x_axis = 0# 更新类属性self.x_axis = new_x_axisself.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)# 强制刷新Canvasself.canvas.update_idletasks()# 更新坐标def update_coordinates(self):"""更新取景框的坐标显示"""titlebar_height = 30border_width = 1correction_value = titlebar_height + border_widthcorrection_left_value = 8# 获取窗口在屏幕上的位置window_x = self.root.winfo_x() + correction_left_valuewindow_y = self.root.winfo_y() + correction_value# 计算取景框在屏幕上的绝对坐标self.topleft_x = window_x + self.x_axis + 1self.topleft_y = window_y + self.y_axis + 1self.bottomright_x = self.topleft_x + self.width - 1self.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 = 1elif new_fps > 100:new_fps = 100self.fps_choose = new_fpsif new_frame < 1:new_frame = 1self.frame_total = new_frameexcept ValueError:# 输入非数字时忽略pass# 录制def toggle_recording(self):if not self.recording:# 开始录制self.recording = Trueself.record_btn.config(text="停止录制", bg="#2ECC71")Screenshot = ScreenshotData()wait = control_frame()# 帧率设置wait.fps = self.fps_chooseself.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+=1if self.frame_count == self.frame_total:self.frame_count = 0self.recording = Falseself.record_btn.config(text="开始录制", bg="#E74C3C")print("耗费时间:",time.time()-self.st)print("秒平均帧:",self.frame_total/(time.time()-self.st))return Trueself.root.after(1, work)self.root.after(1,work)else:# 停止录制self.recording = Falseself.record_btn.config(text="开始录制", bg="#E74C3C")# 窗口拖动功能def start_move(self, event):self.x = event.xself.y = event.ydef stop_move(self, event):self.x = Noneself.y = Nonedef on_move(self, event):deltax = event.x - self.xdeltay = event.y - self.yx = self.root.winfo_x() + deltaxy = self.root.winfo_y() + deltayself.root.geometry(f"+{x}+{y}")# 窗口移动后更新坐标self.update_coordinates()if __name__ == '__main__':root = tk.Tk()app = GIFALL(root)root.mainloop()

http://www.xdnf.cn/news/1028791.html

相关文章:

  • 【互联网基础】互联网公司机房怎么设计
  • Python训练营-Day30-模块和库的导入
  • EDW2025|从传统BI到AI Ready:企业数据与分析能力的实施策略演进
  • Java 锁升级机制详解
  • 芯片测试之VIL/VIH(输入电平)Test全解析:从原理到实战
  • 高通IPA硬件加速介绍
  • 03 | 大模型微调 | 从0学习到实战微调 | 玩转Hugging Face与Transformers框架
  • manpath: can‘t set the locale; make sure $LC_* and $LANG are correct
  • 设备管理-Udev(一)
  • E10集成登录三方系统
  • Python基础补漏
  • ESP32服务器端编码
  • SAM分割一切-使用SAM自动生成对象掩码示例
  • NB/T 32004-2018测试是什么,光伏并网逆变器NB/T 32004测试项目
  • ServiceNow培训第1期
  • MATLAB实现图像纹理特征提取
  • PMP证-介绍
  • 准确--使用 ThinBackup 插件执行备份和恢复
  • Python训练营打卡 Day53
  • 华为数字化转型进阶——精读188页华为EBPM数字化全要素流程管理方法论【附全文阅读】
  • rocketmq producer和consumer连接不同的集群,如何隔离
  • 渗透实战:利用XSS获取cookie和密码
  • JDBC基础关键_002_JDBC 增删改
  • ​​Promise代码理解
  • 如何通过Python从DEM文件生成坡度和坡向数据
  • 读取OpenFOAM二进制polyMesh格式的C/C++程序
  • 手搓中文字符编程语言
  • 一文掌握 Windows 文件传输:5 种命令行工具的原理、参数与示例
  • 中国人工智能证书综合信息表(2025年版)
  • 基于混合预编码的同时进行无线信息和功率传输的毫米波海量MIMO-NOMA