Python实现GIF解析与构建系列(六):自定义tk录制工具详解
目录
Python|GIF 解析与构建(6):手搓 tk 录制工具
一、工具功能概览
二、核心架构设计
1. 帧率控制模块
2. 屏幕捕获模块
3. 主应用模块
三、关键技术解析
1. 屏幕捕获技术
2. 帧率控制原理
3. 透明窗口实现
四、使用指南
1. 基本操作
2. 高级技巧
五、优化方向
六、总结
Python|GIF 解析与构建(6):手搓 tk 录制工具
在 GIF 动图的制作流程中,屏幕录制是一个非常实用的功能。通过 Python 的 Tkinter 库,我们可以轻松构建一个轻量级的 GIF 录制工具,实现自定义区域录制、帧率控制等功能。

一、工具功能概览
我们构建的 GIF 录制工具具备以下核心功能:
这个工具适合用于制作教程演示、软件操作录制等场景,相比专业录制软件更加轻量灵活。
二、核心架构设计
工具采用模块化设计,主要包含三个核心类:
1. 帧率控制模块
control_frame类负责管理录制帧率,确保录制过程保持稳定的帧速率:
该模块通过time.sleep()实现精确的时间控制,确保录制的 GIF 流畅无卡顿。
2. 屏幕捕获模块
ScreenshotData类封装了屏幕截图功能,基于 Windows API 实现:
ctypes调用 GDI32 和 USER32 动态链接库BitBlt函数实现高效屏幕拷贝这个模块解决了 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. 高级技巧
五、优化方向
当前版本的录制工具还有很多可以改进的地方:
- 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) # 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