目录

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.pswp.cn/pingmian/84696.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/84696.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/84696.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在VBA中,提取word表格的文本时,通常有什么干扰符号,需要清除

标题 在VBA中&#xff0c;提取word表格的文本时&#xff0c;通常有什么干扰符号,需要清除 正文 解决问题提取word表格的文本时&#xff0c;通常有什么干扰符号,需要清除 在VBA中提取Word表格文本时&#xff0c;常见的干扰符号及其清除方法如下&#xff1a; ⚠️ 一、主要干扰符…

C++基础学习:深入理解类中的构造函数、析构函数、this指针与new关键字

前言 在C面向对象编程中&#xff0c;类是构建复杂程序的基本单元。今天&#xff0c;我们将深入探讨类中的几个核心概念&#xff1a;构造函数、析构函数、this指针以及new关键字。这些概念对于理解C对象生命周期和内存管理至关重要。 1. 构造函数 构造函数是类的一个特殊成员…

2025 高考游记/总结

坐标GD 新课标一卷选手 前言 思绪有点乱&#xff0c;想想从哪里说起 没想到这个博客已经三年没发过东西了&#xff0c;上次发还是初三准备特长生的时候&#xff0c;一瞬间就已经高考结束了&#xff0c;有种不真实感 对于高中的三年&#xff0c;有很多话、很多感悟想说&#xff…

Python基础之函数(1/3)

函数(基础) [函数后续还会更新两次] 一.认识函数的作用 函数就是将一段具有独立功能的代码块整合到一个整体并命名&#xff0c;在需要的位置&#xff0c;调用这个名称即可完成对应的需求 函数在开发过程中&#xff0c;可以更高效的实现代码重用 二.函数的使用步骤 1定义函…

AWS CloudFormation实战:构建可复用的ECS服务部署模板

一、前言 在云原生时代,基础设施即代码(IaC)已成为DevOps实践的核心组件。AWS CloudFormation作为AWS原生的IaC服务,允许开发人员和系统管理员以声明式方式定义和部署AWS资源。本文将深入探讨如何构建一个通用的CloudFormation模板,用于在AWS ECS(Elastic Container Servic…

GRUB2 启动配置的工作原理与优先级规则详解

一、核心组件概述 /boot/loader/entries/ 类型:目录,存储 BLS (Boot Loader Specification) 格式的启动项配置文件(如 20-custom-kernel-5.14.0.conf)。管理工具:由 grubby、kernel-install 等工具自动生成或修改。配置内容:每个文件定义一个启动项的详细参数(内核路径、…

网页版便签应用开发:HTML5本地存储与拖拽交互实践

文章目录 摘要成品显示核心功能与实现语法1. 本地存储管理2. 拖拽功能实现3. 自动保存机制4. 时间格式化处理 完整代码 摘要 本文详细介绍了一个基于HTML5的便签应用开发过程&#xff0c;重点讲解了如何利用localStorage实现数据持久化存储&#xff0c;以及如何实现流畅的拖拽…

docker compose安装Prometheus、Grafana

1、创建目录结构 mkdir -p /opt/monitoring/{prometheus,grafana} mkdir -p /opt/monitoring/prometheus/{config,data} chmod -R 777 /opt/monitoring # 确保容器有写入权限 2、准备 Prometheus 配置文件 vi /opt/monitoring/prometheus/config/prometheus.yml global:sc…

稀土化合物在生态环境的应用

稀土化合物凭借强吸附性、催化活性及环境兼容性&#xff0c;已成为生态治理的关键材料。氧化物、氯化物、磷酸盐等基础产品&#xff0c;通过灵活复配与工艺适配&#xff0c;可高效解决水体净化、土壤修复、废气处理三大核心问题&#xff0c;推动环境治理向低耗高效转型。那么&a…

搭建网站应该怎样选择服务器?

互联网技术已经全面在各个地区进行发展&#xff0c;越来越多的企业选择线上业务&#xff0c;搭建属于自己的网站运营&#xff0c;以此来增加品牌的知名度并进行详细介绍&#xff0c;但是企业在进行搭建网站的前提&#xff0c;要选择一种合适的服务器&#xff0c;确保后续网站能…

每日算法刷题Day30 6.13:leetcode二分答案2道题,用时1h10min

5. 1201.丑数III(中等) 1201. 丑数 III - 力扣&#xff08;LeetCode&#xff09; 思想 1.丑数是可以被 a 或 b 或 c 整除的 正整数 。 给你四个整数&#xff1a;n 、a 、b 、c &#xff0c;请你设计一个算法来找出第 n 个丑数。 2.此题是4. 878.第N个神奇数字的进阶版&#…

Appium+python自动化(二十一)- Monkey指令操作手机

第一式 - 隐藏命令 monkey隐藏的两个命令&#xff1a; –pck-blacklist-file<黑名单文件><br><br>–pck-whitelist-file<白名单文件> monkey还有一个隐藏的命令那就是&#xff1a; –f<脚本文件>:可以指定monkey的自定义脚本 一般monkey测试…

微信小程序动态效果实战指南:从悬浮云朵到丝滑列表加载

小红书爆款交互设计解析&#xff0c;附完整代码&#xff01; &#x1f525; 一、为什么动态效果是小程序的关键竞争力&#xff1f; 用户留存提升&#xff1a;数据显示&#xff0c;86.3%的微商从业者依赖微信小程序&#xff0c;而动态效果能显著降低跳出率。技术赋能体验&#…

【机器学习】SAE(Sparse Autoencoders)稀疏自编码器

SAE(Sparse Autoencoders)稀疏自编码器 0.引言 大模型一直被视为一个“黑箱”&#xff0c;研究人员对其内部神经元如何相互作用以实现功能的机制尚不清楚。因此研究机理可解释性&#xff08;Mechanistic Interpretability&#xff09;就成为了一个热门研究方向。大模型的复杂…

抖音授权登录-获取用户授权调用凭证

实现微信小程序获取抖音授权,使用Java实现抖音授权登录,您需要使用抖音开放平台提供的API 第一步 :抖音获取授权码 前提条件 •需要去官网为应用申请 scope 的使用权限。•需要在本接口的 scope 传参中填上需要用户授权的 scope,多个 scope 以逗号分割。•用户授权通过后…

普通人怎样用好Deepseek?

今年4月份左右&#xff08;2025年&#xff09;&#xff0c;我在上班路上开车&#xff0c;一边听着「黑客与画家」的播客&#xff0c;一边想着字节的Trae为啥能够远程编程&#xff0c;而我的poclogsender[1] [2]却只能在本地打日志&#xff0c;3天之后&#xff0c;借助deepseek我…

Python ROS2【机器人中间件框架】 简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…

织梦dedecms {dede:sql} LIKE模糊查询问题 多出‘号

我们在用到dede:sql这个标签时候&#xff0c;查询语句中 LIKE %~title~%&#xff0c;~title~这个like后会出现单引号&#xff0c;造成查询出错或者没有结果&#xff0c;下面就需要修改一下sql.lib.php这个文件&#xff0c;我们需要把自动为语句添加单引号去掉。 找到/include/…

Cursor-1.0安装Jupyter-Notebook,可视化运行.ipynb文件中Python分片代码

Cursor 1.0是AI代码编辑器的里程碑的最新版本。 Cursor - AI 代码编辑器 Cursor - The AI Code Editor 下载 Cursor 我使用的Cursor版本信息 Version: 1.0.0 (Universal) VSCode Version: 1.96.2 Commit: 53b99ce608cba35127ae3a050c1738a959750860 Date: 2025-06-04T19:21:39.…

SQL进阶之旅 Day 28:跨库操作与ETL技术

【SQL进阶之旅 Day 28】跨库操作与ETL技术 文章简述 在现代数据驱动的业务场景中&#xff0c;数据往往分布在多个数据库系统中&#xff0c;如MySQL、PostgreSQL、Oracle等。如何高效地进行跨库操作和**数据集成&#xff08;ETL&#xff09;**成为数据工程师和数据库开发人员必…