目录
Python|GIF 解析与构建(8):可视化录制 GIF(完结)
功能概述
核心模块解析
GIF 文件构建模块
帧率控制模块
屏幕捕获模块
主应用模块
关键技术细节
GIF 文件格式解析
颜色转换与量化
LZW 压缩算法
使用方法
总结与优化方向
录制过程
在 GIF 技术探索的最后一篇文章中,我们将实现一个完整的可视化 GIF 录制工具。这个工具能够捕获屏幕指定区域的画面,并将其转换为 GIF 动画,让我们能够轻松记录屏幕操作、生成演示动画或分享有趣的画面。完结撒花!!!!
我们的可视化 GIF 录制工具具有以下核心功能:
整个录制工具由几个关键模块组成,每个模块负责特定的功能,下面我们来逐一解析。
GIFGenerate
类是 GIF 文件构建的核心,它实现了 GIF 文件格式的各个部分:
class GIFGenerate():
def __init__(self):
# 初始化GIF文件头、颜色表、扩展块等基本数据结构
self.fps = 1
self.gif_data = []
self.screen_width = int(100)
self.screen_height = int(100)
# ... 其他初始化代码 ...
这个类处理 GIF 文件的各个组成部分:
control_frame
类负责控制录制帧率,确保捕获的帧速率稳定:
class control_frame():
def __init__(self):
self.start_time = float()
self.fps = int(10)
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):
return time.time() - self.start_time
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)
这个模块通过计算和控制每帧的捕获时间间隔,确保录制的 GIF 动画播放流畅,不会出现帧率忽快忽慢的情况。
ScreenshotData
类使用 Windows API 实现屏幕指定区域的捕获:
class ScreenshotData():
def __init__(self):
self.gdi32 = ctypes.windll.gdi32
self.user32 = ctypes.windll.user32
# 获取屏幕尺寸和缩放比例
# ... 初始化代码 ...
def capture_screen(self, x, y, width, height):
# 获取桌面窗口句柄和设备上下文
hwnd = self.user32.GetDesktopWindow()
hdc_src = self.user32.GetDC(hwnd)
# 创建兼容的内存设备上下文和位图
hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)
bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)
old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)
# 复制屏幕内容到位图
SRCCOPY = 0x00CC0020
self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
# 获取像素数据
# ... 处理像素数据代码 ...
# 清理资源
self.gdi32.SelectObject(hdc_dest, old_bmp)
self.gdi32.DeleteDC(hdc_dest)
self.user32.ReleaseDC(hwnd, hdc_src)
self.gdi32.DeleteObject(bmp)
return pixel_data
这个模块使用 Windows 的 GDI API 实现高效的屏幕捕获,能够获取指定区域的像素数据,为 GIF 录制提供原始图像数据。
GIFALL
类整合了上述所有模块,提供可视化界面:
class GIFALL():
def __init__(self, root):
self.root = root
self.root.title("gif录制")
self.root.geometry("500x250")
self.root.attributes('-topmost', True)
# 初始化录制参数
self.width = 100
self.height = 100
self.x_axis = 0
self.y_axis = 0
self.fps_choose = 10
self.frame_total = 100
self.recording = False
self.gif_data = []
# 创建界面元素
self.create_viewfinder()
self.create_control_panel()
# 绑定窗口事件
self.root.bind("", self.start_move)
self.root.bind("", self.stop_move)
self.root.bind("", self.on_move)
# 启动坐标更新
self.update_coordinates()
这个类创建了一个 Tkinter 界面,包含:
GIF 文件采用二进制格式,主要包含以下几个部分:
在GIFGenerate
类中,我们通过字节操作构建了这些部分,特别注意了字节顺序和格式规范。
GIF 使用索引颜色模式,最多支持 256 种颜色。我们的程序实现了颜色量化功能,将 24 位 RGB 颜色转换为 GIF 支持的索引颜色:
def rgb_deal(self, data):
r = data[2] >> 5 # 3 bits (0-7)
g = data[1] >> 5 # 3 bits (0-7)
b = data[0] >> 6 # 2 bits (0-3)
return (r << 5) | (g << 2) | b
这段代码将 24 位 RGB 颜色转换为 8 位索引颜色,通过位运算减少颜色数量,同时保持视觉上的相似性。
GIF 使用 LZW (Lempel-Ziv-Welch) 压缩算法压缩图像数据。我们在lzw_compress
方法中实现了这一算法:
def lzw_compress(self, data):
dictionary = {bytes([i]): i for i in range(256)}
next_code = 258 # 256/257 保留
w = bytes()
buffer = 0
bits = 0
compressed = bytearray()
code_size = 9 # 初始位宽 9 位
for byte in data:
c = bytes([byte])
wc = w + c
if wc in dictionary:
w = wc
else:
# 写入当前w的编码
buffer |= dictionary[w] << bits
bits += code_size
# 按字节刷新缓冲区
while bits >= 8:
compressed.append(buffer & 0xFF)
buffer >>= 8
bits -= 8
# 添加新条目到字典
if next_code < 4096:
dictionary[wc] = next_code
next_code += 1
# 动态增加位宽
if next_code > (1 << code_size) and code_size < 12:
code_size += 1
w = c
# 处理最后残留的w
if w:
buffer |= dictionary[w] << bits
bits += code_size
# 写入结束标记
buffer |= 257 << bits
bits += code_size
# 清空缓冲区
while bits > 0:
compressed.append(buffer & 0xFF)
buffer >>= 8
bits -= 8 if bits >= 8 else bits
return bytes(compressed)
LZW 算法通过构建字典来压缩重复出现的数据模式,特别适合 GIF 这种包含大面积单一颜色的图像格式。
使用这个 GIF 录制工具非常简单:
通过这个项目,我们实现了从屏幕捕获到 GIF 生成的完整流程,深入理解了 GIF 文件格式和相关算法。这个工具已经可以满足基本的 GIF 录制需求,但还有一些可以优化的方向:
完结撒花!!!!
以下是全部代码:
import time import ctypes import tkinter as tk from tkinter import filedialog from datetime import datetime class GIFGenerate(): def __init__(self): # 提供数据 # 帧率 self.fps = 1 self.gif_data = [] # gif数据对象 self.screen_width = int(100) # 宽度 self.screen_height = int(100) # 长度 self.data = [] # 基础数据 顺序安排 # 开头 宽 长 标志参数(计算) 背景颜色索引 宽高比 self.header = 0 self.header_sign = 0 """ m 占位 7 → 需左移 7 位(m << 7)全局颜色表; cr 占位 6-4 → 需左移 4 位(cr << 4)颜色深度; s 占位 3 → 需左移 3 位(s << 3)分类标志; pixel 占位 2-0 → 无需位移(直接取 pixel) """ self.header_transparent_color = int(0).to_bytes(1, byteorder="little") self.header_ratio = int(0).to_bytes(1, byteorder="little") # 全局颜色表 颜色提取(计算) self.global_color_table = b"" # 拓展块 应用程序扩展 块长度 扩展应用标记符 子块长度 循环类型 循环参数 self.application_sign = b"\x21" + b"\xFF" self.application_block_number = int(11).to_bytes(1, byteorder="little") self.application_marker = b"NETSCAPE2.0" self.application_sub_block_number = int(3).to_bytes(1, byteorder="little") self.application_loop_kind = int(1).to_bytes(1, byteorder="little") self.application_loop_parameters = int(0).to_bytes(2, byteorder="little") """ 0x00 00 表示 无限循环。 若为 0x01 00,则表示循环 1 次(播放 2 次后停止)。 若为 0x05 00,则表示循环 5 次(播放 6 次后停止)。 """ self.application_sign_end = b"\x00" # 拓展块 图形控制扩展 子块长度 标志参数 延迟时间(计算) 透明色索引 self.control_sign = b"\x21" + b"\xF9" self.control_block_number = int(4).to_bytes(1, byteorder="little") self.control_parameters = int(0).to_bytes(1, byteorder="little") """ - 第 7 位(最高位):透明度标志(0 = 关闭) - 第 6 位:用户输入标志(0 = 无需等待输入) - 第 3-5 位: disposal 方法(0 = 不指定,默认保留) - 第 0-2 位:保留位(0) """ self.control_time = 0 self.control_transparent_color = int(0).to_bytes(1, byteorder="little") self.control_sign_end = b"\x00" # 图像标识符 x偏移 y偏移 宽度 长度 标志参数 self.image_sign = b"\x2C" self.image_offset_x = int(0).to_bytes(2, byteorder="little") self.image_offset_y = int(0).to_bytes(2, byteorder="little") self.image_width = 0 self.image_length = 0 self.image_sign_parameters = int(0).to_bytes(1, byteorder="little") # 局部颜色表 局部颜色(不处理) # 图像内容 压缩位数 子块长度 self.image_compress = int(8).to_bytes(1, byteorder="little") self.image_sub_block_length = b"" self.image_content = b"" # 长度 数据 长度 数据 self.image_sign_end = b"\x00" self.batch_color = b"" # 结束标识符 self.end_sign = b"\x3B" def start(self): self.header = (b"GIF89a" + int(self.screen_width).to_bytes(2, byteorder="little") + int(self.screen_height).to_bytes(2, byteorder="little")) self.image_width = int(self.screen_width).to_bytes(2, byteorder="little") self.image_length = int(self.screen_height).to_bytes(2, byteorder="little") self.control_time = int(100 / self.fps).to_bytes(2, byteorder="little") # 处理rgb def rgb_deal(self, data): r = data[2] >> 5 # 3 bits (0-7) g = data[1] >> 5 # 3 bits (0-7) b = data[0] >> 6 # 2 bits (0-3) return (r << 5) | (g << 2) | b # 计算 def conversion(self): for one in self.gif_data: list_one = [one[i:i + 3] for i in range(0, len(one), 3)] self.data.append(list_one) # 展开 list_color = [k for i in self.data for k in i] # 二维展开 实际上可以算作一维 # 处理成1字节颜色 list_deal = [self.rgb_deal(bytes(i)) for i in list_color] # 去重 list_set = set(list_deal) # 生成字典转换 hash_map = {key: (0, 0, 0) for key in list_set} # 全部单个遍历映射字典 for s in list_set: hash_map[s] = list_color[list_deal.index(s)] # 字典赋值 key_mapping = {key: i for i, key in enumerate(hash_map.keys())} print(key_mapping) # 获取全局颜色表 color_data = list(hash_map.values()) color_number = len(color_data) color_number_change = color_number # 强制色彩数量 if color_number <= 4: color_number_change = 4 binary_number = (color_number_change - 1).bit_length() # 填充色数量 fill_color_number = 2 ** binary_number - color_number print(fill_color_number) # 计算数量 self.header_sign = 0 self.header_sign |= (1 << 7) self.header_sign |= (3 << 4) self.header_sign |= (0 << 3) self.header_sign |= (binary_number - 1 << 0) # 需处理 self.header_sign = int(self.header_sign).to_bytes(1, byteorder="little") print("全局颜色数量:", 2 ** ((self.header_sign[0] & 0b00000111) + 1)) def bgr_to_rgb(bgr_list): rgb_list = [] for bgr in bgr_list: # 提取BGR值并转换为RGB rgb = [bgr[2], bgr[1], bgr[0]] rgb_list.append(rgb) return rgb_list color_data = bgr_to_rgb(color_data) # 展平元组并转换为字节 color_data_list = [value for tup in color_data for value in tup] + fill_color_number * [0, 0, 0] # 填充色 print(color_data_list) self.global_color_table = bytes(color_data_list) print(self.global_color_table) # 按照长宽转换回去 这里是序列号 serial_number = [key_mapping.get(x, x) for x in list_deal] print(serial_number) reshaped = [] group_size = int(self.screen_width) * int(self.screen_height) for i in range(0, len(serial_number), group_size): reshaped.append(serial_number[i:i + group_size]) self.data = reshaped print(self.data) pass # 批量图像颜色数据 def batch_color_deal(self): self.batch_color = b"" # 将所有序列进行分批处理 然后拼合在一起 # 对象数量遍历处理 for one in range(len(self.gif_data)): # 将其转换成字节 data = bytes([i for i in self.data[one]]) print(data) data_deal = self.lzw_compress(data) print("压缩码:", data_deal) # 处理好的数据需要分解开来,因为长度是1字节需要处理 长度 数据 长度 数据 # 因为 0x00 是结束符号 所以最大应该是255 def deal_Byte(byte): byte_deal = b"" length = len(byte) max_length = length // 255 remaining = length % 255 > 0 start = 0 for i in range(max_length): max_length_sign = b"\xFF" byte_deal += max_length_sign + byte[start:start + 255] start += 255 if remaining: sign = int(length % 255).to_bytes(1, byteorder="little") byte_deal += sign + byte[start:] return bytes(byte_deal) # 核心数据 image_core_data = deal_Byte(data_deal) self.image_sign = b"\x2C" self.image_offset_x = int(0).to_bytes(2, byteorder="little") self.image_offset_y = int(0).to_bytes(2, byteorder="little") self.image_width = int(self.screen_width).to_bytes(2, byteorder="little") self.image_length = int(self.screen_height).to_bytes(2, byteorder="little") self.image_sign_parameters = int(0).to_bytes(1, byteorder="little") # 局部颜色表 局部颜色(不处理) # 图像内容 压缩位数 子块长度 self.image_compress = int(8).to_bytes(1, byteorder="little") self.image_sub_block_length = b"" self.image_content = b"" # 长度 数据 长度 数据 self.image_sign_end = b"\x00" # 合成批量 self.batch_color += (self.image_sign + self.image_offset_x + self.image_offset_y + self.image_width + self.image_length + self.image_sign_parameters + self.image_compress + image_core_data + self.image_sign_end) pass # 保存 def gif_save(self, path): # 开头数据 gif_data = self.header + self.header_sign + self.header_transparent_color + self.header_ratio + self.global_color_table # 应用程序扩展 gif_data += self.application_sign + self.application_block_number + self.application_marker + self.application_sub_block_number + self.application_loop_kind + self.application_loop_parameters + self.application_sign_end # 图形控制扩展 gif_data += self.control_sign + self.control_block_number + self.control_parameters + self.control_time + self.control_transparent_color + self.control_sign_end # 图像数据 gif_data += self.batch_color # 结束 gif_data += self.end_sign print(path) with open(path, "wb") as f: f.write(gif_data) print("保存成功") pass # 压缩 def lzw_compress(self, data): dictionary = {bytes([i]): i for i in range(256)} next_code = 258 # 256/257 保留 w = bytes() buffer = 0 bits = 0 compressed = bytearray() code_size = 9 # 初始位宽 9 位 for byte in data: c = bytes([byte]) wc = w + c if wc in dictionary: w = wc else: # 写入当前 w 的编码 buffer |= dictionary[w] << bits bits += code_size # 按字节刷新缓冲区 while bits >= 8: compressed.append(buffer & 0xFF) buffer >>= 8 bits -= 8 # 添加新条目 if next_code < 4096: # 限制最大字典大小 dictionary[wc] = next_code next_code += 1 # 动态增加位宽 (9->10->11->12) if next_code > (1 << code_size) and code_size < 12: code_size += 1 w = c # 处理最后残留的 w if w: buffer |= dictionary[w] << bits bits += code_size # 写入结束标记 (257) buffer |= 257 << bits bits += code_size # 清空缓冲区 while bits > 0: compressed.append(buffer & 0xFF) buffer >>= 8 bits -= 8 if bits >= 8 else bits return bytes(compressed) # 控制帧率 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.gif_data = [] # 左上右下坐标 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("", self.start_move) self.root.bind(" ", self.stop_move) self.root.bind(" ", 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.gif_data = [] # 开始录制 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() self.gif_data.append(data) # 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 = True self.record_btn.config(text="保存", bg="#FFA500") 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") file_path = filedialog.asksaveasfilename( initialfile=datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), defaultextension=".gif", filetypes=[("GIF files", "*.gif"), ("All files", "*.*")]) if file_path: # 处理数据 g = GIFGenerate() g.fps = self.fps_choose g.screen_width = self.width g.screen_height = self.height g.gif_data = self.gif_data g.start() g.conversion() # 计算 g.batch_color_deal() # 处理图像 g.gif_save(file_path) # 窗口拖动功能 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
from tkinter import filedialog
from datetime import datetime
class GIFGenerate():
def __init__(self):
# 提供数据
# 帧率
self.fps = 1
self.gif_data = [] # gif数据对象
self.screen_width = int(100) # 宽度
self.screen_height = int(100) # 长度
self.data = []
# 基础数据 顺序安排
# 开头 宽 长 标志参数(计算) 背景颜色索引 宽高比
self.header = 0
self.header_sign = 0
"""
m 占位 7 → 需左移 7 位(m << 7)全局颜色表;
cr 占位 6-4 → 需左移 4 位(cr << 4)颜色深度;
s 占位 3 → 需左移 3 位(s << 3)分类标志;
pixel 占位 2-0 → 无需位移(直接取 pixel)
"""
self.header_transparent_color = int(0).to_bytes(1, byteorder="little")
self.header_ratio = int(0).to_bytes(1, byteorder="little")
# 全局颜色表 颜色提取(计算)
self.global_color_table = b""
# 拓展块 应用程序扩展 块长度 扩展应用标记符 子块长度 循环类型 循环参数
self.application_sign = b"\x21" + b"\xFF"
self.application_block_number = int(11).to_bytes(1, byteorder="little")
self.application_marker = b"NETSCAPE2.0"
self.application_sub_block_number = int(3).to_bytes(1, byteorder="little")
self.application_loop_kind = int(1).to_bytes(1, byteorder="little")
self.application_loop_parameters = int(0).to_bytes(2, byteorder="little")
"""
0x00 00 表示 无限循环。
若为 0x01 00,则表示循环 1 次(播放 2 次后停止)。
若为 0x05 00,则表示循环 5 次(播放 6 次后停止)。
"""
self.application_sign_end = b"\x00"
# 拓展块 图形控制扩展 子块长度 标志参数 延迟时间(计算) 透明色索引
self.control_sign = b"\x21" + b"\xF9"
self.control_block_number = int(4).to_bytes(1, byteorder="little")
self.control_parameters = int(0).to_bytes(1, byteorder="little")
"""
- 第 7 位(最高位):透明度标志(0 = 关闭)
- 第 6 位:用户输入标志(0 = 无需等待输入)
- 第 3-5 位: disposal 方法(0 = 不指定,默认保留)
- 第 0-2 位:保留位(0)
"""
self.control_time = 0
self.control_transparent_color = int(0).to_bytes(1, byteorder="little")
self.control_sign_end = b"\x00"
# 图像标识符 x偏移 y偏移 宽度 长度 标志参数
self.image_sign = b"\x2C"
self.image_offset_x = int(0).to_bytes(2, byteorder="little")
self.image_offset_y = int(0).to_bytes(2, byteorder="little")
self.image_width = 0
self.image_length = 0
self.image_sign_parameters = int(0).to_bytes(1, byteorder="little")
# 局部颜色表 局部颜色(不处理)
# 图像内容 压缩位数 子块长度
self.image_compress = int(8).to_bytes(1, byteorder="little")
self.image_sub_block_length = b""
self.image_content = b""
# 长度 数据 长度 数据
self.image_sign_end = b"\x00"
self.batch_color = b""
# 结束标识符
self.end_sign = b"\x3B"
def start(self):
self.header = (b"GIF89a" +
int(self.screen_width).to_bytes(2, byteorder="little") +
int(self.screen_height).to_bytes(2, byteorder="little"))
self.image_width = int(self.screen_width).to_bytes(2, byteorder="little")
self.image_length = int(self.screen_height).to_bytes(2, byteorder="little")
self.control_time = int(100 / self.fps).to_bytes(2, byteorder="little")
# 处理rgb
def rgb_deal(self, data):
r = data[2] >> 5 # 3 bits (0-7)
g = data[1] >> 5 # 3 bits (0-7)
b = data[0] >> 6 # 2 bits (0-3)
return (r << 5) | (g << 2) | b
# 计算
def conversion(self):
for one in self.gif_data:
list_one = [one[i:i + 3] for i in range(0, len(one), 3)]
self.data.append(list_one)
# 展开
list_color = [k for i in self.data for k in i] # 二维展开 实际上可以算作一维
# 处理成1字节颜色
list_deal = [self.rgb_deal(bytes(i)) for i in list_color]
# 去重
list_set = set(list_deal)
# 生成字典转换
hash_map = {key: (0, 0, 0) for key in list_set}
# 全部单个遍历映射字典
for s in list_set:
hash_map[s] = list_color[list_deal.index(s)] # 字典赋值
key_mapping = {key: i for i, key in enumerate(hash_map.keys())}
print(key_mapping)
# 获取全局颜色表
color_data = list(hash_map.values())
color_number = len(color_data)
color_number_change = color_number
# 强制色彩数量
if color_number <= 4:
color_number_change = 4
binary_number = (color_number_change - 1).bit_length()
# 填充色数量
fill_color_number = 2 ** binary_number - color_number
print(fill_color_number)
# 计算数量
self.header_sign = 0
self.header_sign |= (1 << 7)
self.header_sign |= (3 << 4)
self.header_sign |= (0 << 3)
self.header_sign |= (binary_number - 1 << 0) # 需处理
self.header_sign = int(self.header_sign).to_bytes(1, byteorder="little")
print("全局颜色数量:", 2 ** ((self.header_sign[0] & 0b00000111) + 1))
def bgr_to_rgb(bgr_list):
rgb_list = []
for bgr in bgr_list:
# 提取BGR值并转换为RGB
rgb = [bgr[2], bgr[1], bgr[0]]
rgb_list.append(rgb)
return rgb_list
color_data = bgr_to_rgb(color_data)
# 展平元组并转换为字节
color_data_list = [value for tup in color_data for value in tup] + fill_color_number * [0, 0, 0] # 填充色
print(color_data_list)
self.global_color_table = bytes(color_data_list)
print(self.global_color_table)
# 按照长宽转换回去 这里是序列号
serial_number = [key_mapping.get(x, x) for x in list_deal]
print(serial_number)
reshaped = []
group_size = int(self.screen_width) * int(self.screen_height)
for i in range(0, len(serial_number), group_size):
reshaped.append(serial_number[i:i + group_size])
self.data = reshaped
print(self.data)
pass
# 批量图像颜色数据
def batch_color_deal(self):
self.batch_color = b""
# 将所有序列进行分批处理 然后拼合在一起
# 对象数量遍历处理
for one in range(len(self.gif_data)):
# 将其转换成字节
data = bytes([i for i in self.data[one]])
print(data)
data_deal = self.lzw_compress(data)
print("压缩码:", data_deal)
# 处理好的数据需要分解开来,因为长度是1字节需要处理 长度 数据 长度 数据
# 因为 0x00 是结束符号 所以最大应该是255
def deal_Byte(byte):
byte_deal = b""
length = len(byte)
max_length = length // 255
remaining = length % 255 > 0
start = 0
for i in range(max_length):
max_length_sign = b"\xFF"
byte_deal += max_length_sign + byte[start:start + 255]
start += 255
if remaining:
sign = int(length % 255).to_bytes(1, byteorder="little")
byte_deal += sign + byte[start:]
return bytes(byte_deal)
# 核心数据
image_core_data = deal_Byte(data_deal)
self.image_sign = b"\x2C"
self.image_offset_x = int(0).to_bytes(2, byteorder="little")
self.image_offset_y = int(0).to_bytes(2, byteorder="little")
self.image_width = int(self.screen_width).to_bytes(2, byteorder="little")
self.image_length = int(self.screen_height).to_bytes(2, byteorder="little")
self.image_sign_parameters = int(0).to_bytes(1, byteorder="little")
# 局部颜色表 局部颜色(不处理)
# 图像内容 压缩位数 子块长度
self.image_compress = int(8).to_bytes(1, byteorder="little")
self.image_sub_block_length = b""
self.image_content = b""
# 长度 数据 长度 数据
self.image_sign_end = b"\x00"
# 合成批量
self.batch_color += (self.image_sign +
self.image_offset_x +
self.image_offset_y +
self.image_width +
self.image_length +
self.image_sign_parameters +
self.image_compress +
image_core_data +
self.image_sign_end)
pass
# 保存
def gif_save(self, path):
# 开头数据
gif_data = self.header + self.header_sign + self.header_transparent_color + self.header_ratio + self.global_color_table
# 应用程序扩展
gif_data += self.application_sign + self.application_block_number + self.application_marker + self.application_sub_block_number + self.application_loop_kind + self.application_loop_parameters + self.application_sign_end
# 图形控制扩展
gif_data += self.control_sign + self.control_block_number + self.control_parameters + self.control_time + self.control_transparent_color + self.control_sign_end
# 图像数据
gif_data += self.batch_color
# 结束
gif_data += self.end_sign
print(path)
with open(path, "wb") as f:
f.write(gif_data)
print("保存成功")
pass
# 压缩
def lzw_compress(self, data):
dictionary = {bytes([i]): i for i in range(256)}
next_code = 258 # 256/257 保留
w = bytes()
buffer = 0
bits = 0
compressed = bytearray()
code_size = 9 # 初始位宽 9 位
for byte in data:
c = bytes([byte])
wc = w + c
if wc in dictionary:
w = wc
else:
# 写入当前 w 的编码
buffer |= dictionary[w] << bits
bits += code_size
# 按字节刷新缓冲区
while bits >= 8:
compressed.append(buffer & 0xFF)
buffer >>= 8
bits -= 8
# 添加新条目
if next_code < 4096: # 限制最大字典大小
dictionary[wc] = next_code
next_code += 1
# 动态增加位宽 (9->10->11->12)
if next_code > (1 << code_size) and code_size < 12:
code_size += 1
w = c
# 处理最后残留的 w
if w:
buffer |= dictionary[w] << bits
bits += code_size
# 写入结束标记 (257)
buffer |= 257 << bits
bits += code_size
# 清空缓冲区
while bits > 0:
compressed.append(buffer & 0xFF)
buffer >>= 8
bits -= 8 if bits >= 8 else bits
return bytes(compressed)
# 控制帧率
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.gif_data = []
# 左上右下坐标
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("", self.start_move)
self.root.bind("", self.stop_move)
self.root.bind("", 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.gif_data = []
# 开始录制
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()
self.gif_data.append(data)
# 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 = True
self.record_btn.config(text="保存", bg="#FFA500")
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")
file_path = filedialog.asksaveasfilename(
initialfile=datetime.now().strftime("%Y-%m-%d-%H-%M-%S"),
defaultextension=".gif",
filetypes=[("GIF files", "*.gif"), ("All files", "*.*")])
if file_path:
# 处理数据
g = GIFGenerate()
g.fps = self.fps_choose
g.screen_width = self.width
g.screen_height = self.height
g.gif_data = self.gif_data
g.start()
g.conversion() # 计算
g.batch_color_deal() # 处理图像
g.gif_save(file_path)
# 窗口拖动功能
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()