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. 基本操作
- 启动程序后,会看到一个透明的录制窗口
- 通过输入框设置录制区域的宽度、高度和坐标
- 设置合适的帧率(建议 10-30fps)和总帧数
- 点击 "开始录制" 按钮开始录制
- 录制完成后会显示总耗时和平均帧率
2. 高级技巧
- 拖动窗口可以调整录制区域的位置
- 实时坐标显示帮助精确定位录制区域
- 根据录制内容特性调整帧率:
- 静态内容:10-15fps 即可
- 动态内容:24-30fps 更流畅
- 总帧数控制录制时长:时长 = 总帧数 / 帧率
五、优化方向
当前版本的录制工具还有很多可以改进的地方:
- GIF 生成功能:当前只完成了屏幕捕获,需要添加像素数据到 GIF 的转换功能
- 文件保存:增加录制结果保存功能,支持自定义文件名和保存路径
- 区域选择优化:添加鼠标拖动选择区域的功能,提升操作便捷性
- 跨平台支持:当前仅支持 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()